Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 64 additions & 15 deletions server/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,9 @@ def __init__(self, num_players, trump_rank, bottom_player, deck_name=None):
# list of cards that each player has placed on the board for the current trick
self.board = [[] for i in range(num_players)]

# the full list of cards in a failed flush for the current trick, if any
self.failed_flush = []

# history of all declarations
# to get the most recent declaration, use self.declaration
self.declarations = []
Expand Down Expand Up @@ -405,6 +408,7 @@ def is_starting_trick(self):
def clear_board(self):
for i in range(len(self.board)):
self.board[i] = []
self.failed_flush = []

def get_trick_points(self):
points = 0
Expand Down Expand Up @@ -449,7 +453,9 @@ def get_player_view(self, player):
'bottom_size': BOTTOM_SIZE[self.num_players],
'player_points': self.player_points,
'attacking_players': self.attacking_players,
'bottom_player': self.bottom_player
'bottom_player': self.bottom_player,
'trick_first_player': self.trick_first_player,
'failed_flush': [card.dict for card in self.failed_flush],
}

if player is not None:
Expand Down Expand Up @@ -487,35 +493,75 @@ def get_suit_tractors_from_cards(self, cards, trick_suit):
suit_tractors = cards_to_tractors(suit_cards, trick_suit, self.trump_card)
return suit_tractors

def is_flush_tractor_beaten(self, player, trick_suit, tractor):
'''
Determines whether another player has cards in their hand matching the tractor's suit
that form a tractor beating the provided tractor.
'''
for i, hand in enumerate(self.player_hands):
if i == player:
continue
hand_suit_tractors = self.get_suit_tractors_from_cards(self.player_hands[i], trick_suit)
for t in hand_suit_tractors:
# to beat tractor, t must have at least the same rank/length, while having higher power (card value)
if t.rank < tractor.rank or t.length < tractor.length:
continue
if t.power <= tractor.power:
continue
return True

return False

def is_play_valid(self, player, cards):
'''
This function checks if player's cards, aka play, is valid. In order for a play to be valid, the play
must follow suit and form of the trick's first play. Otherwise, the appropriate form is calculated
and used to determine whether or not the play is valid given the trick's first play.

It returns a tuple (out_cards, valid). If the play is valid, out_cards is the list of played cards,
and valid=True; out_cards may not match cards for a flush, in the case that some tractor in the
flush was not high enough. If the play is not valid, out_cards=None and valid=False.

Args:
player: int
cards: Card []
Returns:
bool
(Card [], bool)
'''
play_card_count = len(cards)
# number of cards must be nonzero
if play_card_count == 0:
return False
return None, False

# TODO(workitem0028): once flushing feature is added, then multiple tractors is allowed if player wants to flush
# for now, first play must be one tractor
if player == self.trick_first_player:
# need the first card's suit in order to accurately transform cards to tractors if board is empty
return len(cards_to_tractors(cards, cards[0].suit, self.trump_card)) == 1
trick_suit = cards[0].get_normalized_suit(self.trump_card)
tractors = cards_to_tractors(cards, trick_suit, self.trump_card)
# first, all tractors must be the same suit
for tractor in tractors[1:]:
if tractor.suit_type != tractors[0].suit_type:
return None, False
# if there is only one tractor, then at this point it's valid
if len(tractors) == 1:
return cards, True
# otherwise, this is a flush
# for flush plays, other players must not have tractors in their hand in the suit that
# beat any components of the flush.
# otherwise, the player is forced to play the weakest tractor that was beaten.
# in both cases, the play is valid
beaten_tractors = [tractor for tractor in tractors if self.is_flush_tractor_beaten(player, trick_suit, tractor)]
if len(beaten_tractors) == 0:
return cards, True
smallest_beaten_tractor = min(beaten_tractors)
cards = [card for l in smallest_beaten_tractor.orig_cards for card in l]
return cards, True

first_play = self.board[self.trick_first_player]
trick_card_count = len(first_play)

# number of cards played must match number of cards in trick
if play_card_count != trick_card_count:
return False
return None, False

# grab trick tractor and player hand trick suit tractor rank and length data
trick_card = first_play[0]
Expand All @@ -526,7 +572,7 @@ def is_play_valid(self, player, cards):
# if hand doesn't have any trick suit cards then player can play cards of any suit as long as
# play_card_count equals trick_card_count (case already handled above)
if not hand_suit_tractors:
return True
return cards, True

play_suit_cards = [card for card in cards if card.get_normalized_suit(self.trump_card) == trick_suit]
play_suit_tractors = cards_to_tractors(play_suit_cards, trick_suit, self.trump_card)
Expand All @@ -543,17 +589,17 @@ def is_play_valid(self, player, cards):

play_idx = find_matching_data_index(play_data_array, trick_data_array[0])
if play_idx is None:
return False
return None, False

play_min_data = get_min_data(trick_data_array[0], play_data_array[play_idx])
if play_min_data < hand_min_data:
return False
return None, False

trick_data_array = update_data_array(trick_data_array, hand_min_data)
hand_data_array = update_data_array(hand_data_array, hand_min_data)
play_data_array = update_data_array(play_data_array, hand_min_data)

return True
return cards, True

class RoundListener(object):
def round_started(self, r):
Expand Down Expand Up @@ -709,11 +755,14 @@ def play(self, player, cards):
self.state.trick_first_player = player

# checks if play is invalid
if not self.state.is_play_valid(player, cards):
play_cards, valid = self.state.is_play_valid(player, cards)
if not valid:
raise RoundException("Invalid play")

self.state.board[player] = cards
self.state.remove_cards_from_hand(player, cards)
if len(play_cards) != len(cards):
self.state.failed_flush = cards
self.state.board[player] = play_cards
self.state.remove_cards_from_hand(player, play_cards)

# if all players have played, then we need to figure out who won to update the turn
# otherwise, we can just increment it
Expand All @@ -725,7 +774,7 @@ def play(self, player, cards):
else:
self.state.increment_turn()

self._fire(lambda listener: listener.player_played(self, player, cards))
self._fire(lambda listener: listener.player_played(self, player, play_cards))

def set_bottom(self, player, cards):
'''
Expand Down
35 changes: 27 additions & 8 deletions server/model_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ def testRoundEnd(self):
trick_player = (trick_player + 1) % self.num_players

self.assertEqual(round.state.status, STATUS_ENDED)

def testFirstPlayerSetToBottomPlayer(self):
# Mock model.create_random_deck to use a deterministic deck.
with mock.patch('model.create_random_deck', return_value=create_deck(self.num_decks)):
Expand Down Expand Up @@ -422,8 +422,6 @@ def testSuitTractorsFromHandJokerTrump(self, suit_name, trick_card, suit_tractor

['different suits 1 pair + 1 single', [Card('d', '2'), Card('d', '2'), Card('s', '5')]],

['same suits 2 nonconsecutive pairs', [Card('h', '2'), Card('h', '2'), Card('h', '6'), Card('h', '6')]],

['different suits 2 consecutive pairs + single',
[Card('d', '4'), Card('d', '4'), Card('d', '5'), Card('d', '5'), Card('c', '5')]],

Expand All @@ -435,7 +433,7 @@ def testSuitTractorsFromHandJokerTrump(self, suit_name, trick_card, suit_tractor
])

def testInvalidFirstPlays(self, name, play):
self.assertFalse(self.round_state.is_play_valid(self.first_player, play))
self.assertFalse(self.round_state.is_play_valid(self.first_player, play)[1])

@parameterized.expand([
['1 single', [Card('h', '2')]],
Expand All @@ -462,8 +460,8 @@ def testValidFirstPlays(self, name, play):

def testFollowSuitValidity(self, name, first_play, invalid_play, valid_play):
self.round_state.board[0] = first_play
self.assertFalse(self.round_state.is_play_valid(self.second_player, invalid_play))
self.assertTrue(self.round_state.is_play_valid(self.second_player, valid_play))
self.assertFalse(self.round_state.is_play_valid(self.second_player, invalid_play)[1])
self.assertTrue(self.round_state.is_play_valid(self.second_player, valid_play)[1])

@parameterized.expand(model_test_data.follow_suit_validity_custom_hand_test_data)

Expand All @@ -472,9 +470,30 @@ def testFollowSuitValidityWithCustomHand(self, name, first_play, hand, invalid_p
self.round_state.board[0] = first_play
self.round_state.player_hands[self.third_player] = hand
if invalid_play is not None:
self.assertFalse(self.round_state.is_play_valid(self.third_player, invalid_play))
self.assertFalse(self.round_state.is_play_valid(self.third_player, invalid_play)[1])
if valid_play is not None:
self.assertTrue(self.round_state.is_play_valid(self.third_player, valid_play))
self.assertTrue(self.round_state.is_play_valid(self.third_player, valid_play)[1])

@parameterized.expand([
['two singles neither beaten', 'Qh Ah', None],

['two singles one beaten', 'Qs As', 'Qs'],

['two doubles neither beaten', 'Qs Qs As As', None],

['three singles, two beaten, lowest played', 'Js Qs As', 'Js'],

['three tractors, two beaten, lowest order played', '4s 4s Qs As', 'Qs'],
])

def testFlushValidity(self, name, play, expect):
play = test_utils.cards_from_str(play)
cards, valid = self.round_state.is_play_valid(self.first_player, play)
self.assertTrue(valid)
if expect is None:
self.assertEqual(cards, play)
else:
self.assertEqual(cards, test_utils.cards_from_str(expect))

@parameterized.expand([
['no play', [], []],
Expand Down
25 changes: 21 additions & 4 deletions web/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,27 @@ h1, h2, h3 {
line-height: 1.2;
}

.playerCards {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 100%;
}

.playerCards .annotationLabel {
padding-left: 5px;
padding-right: 5px;
}

.playingCards .card.trump {
background-color: #ffec72;
}

.playingCards .card.annotation {
background-color: lightgray;
}

.inHand .card {
transition: 0.2s;
}
Expand Down Expand Up @@ -249,8 +266,8 @@ h1, h2, h3 {
text-rendering: optimizeLegibility;
color: #546cca;
letter-spacing: .05em;
text-shadow:
4px 4px 0px #d5d5d5,
text-shadow:
4px 4px 0px #d5d5d5,
7px 7px 0px rgba(0, 0, 0, 0.2);
-webkit-mask-image: -webkit-gradient(linear, left 55%, left bottom, from(rgba(0,0,0,1)), to(rgba(0,0,0,0)));
}
Expand All @@ -270,7 +287,7 @@ a:visited {

.gameTable {
border-collapse: collapse;
margin-left: auto;
margin-left: auto;
margin-right: auto;
margin-top: 20px;
width: 50%;
Expand Down Expand Up @@ -308,7 +325,7 @@ a:visited {
.nameBox {
margin-bottom: 15px;
}

/* Style the input fields */
input, select {
vertical-align: middle;
Expand Down
48 changes: 34 additions & 14 deletions web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ <h2 class="title retroshadow">80 POINTS</h2>
</td>
<td class="occupied">
{{ game.occupied }}/{{ game.total }}
</td>
</td>
</tr>
</table>
</div>
Expand Down Expand Up @@ -100,18 +100,38 @@ <h2 class="title retroshadow">80 POINTS</h2>
{{ player.name }}
</template>
</p>
<div class="playingCards fourColours">
<ul class="hand" :style="handStyle(board[index])">
<transition name="fade">
<div v-if="board[index] && board[index].length > 0">
<li v-for="card in board[index]">
<card :suit="card.suit" :rank="card.value"
:trump-suit="trumpSuit" :trump-rank="trumpRank">
</card>
</li>
</div>
</transition>
</ul>
<div class="playerCards">
<template v-if="trickFirstPlayer == index && failedFlush && failedFlush.length > 0">
<div class="annotationLabel">(</div>
<div class="playingCards fourColours">
<ul class="hand" :style="handStyle(failedFlush)">
<transition name="fade">
<div>
<li v-for="card in failedFlush">
<card :suit="card.suit" :rank="card.value"
:trump-suit="trumpSuit" :trump-rank="trumpRank"
:annotation="true">
</card>
</li>
</div>
</transition>
</ul>
</div>
<div class="annotationLabel">)</div>
</template>
<div class="playingCards fourColours">
<ul class="hand" :style="handStyle(board[index])">
<transition name="fade">
<div v-if="board[index] && board[index].length > 0">
<li v-for="card in board[index]">
<card :suit="card.suit" :rank="card.value"
:trump-suit="trumpSuit" :trump-rank="trumpRank">
</card>
</li>
</div>
</transition>
</ul>
</div>
</div>
</div>
</div>
Expand All @@ -137,7 +157,7 @@ <h2>Defenders Win</h2>
</card>
</li>
</ul>
</div>
</div>
</div>
<div class="game-footer">
<template v-if="cards.length > 0">
Expand Down
Loading