Пример #1
0
class Expeditions(SeatedGame):
    """A Expeditions game table implementation.  Based on a game invented in
    1999 by Reiner Knizia.
    """

    def __init__(self, server, table_name):

        super(Expeditions, self).__init__(server, table_name)

        self.game_display_name = "Expeditions"
        self.game_name = "expeditions"
        self.seats = [
            Seat("Left"),
            Seat("Right")
        ]
        self.min_players = 2
        self.max_players = 2
        self.state = State("need_players")
        self.prefix = "(^RExpeditions^~): "
        self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name)

        # Expeditions-specific stuff.
        self.suit_count = 5
        self.agreement_count = 3
        self.penalty = 20
        self.bonus = True
        self.bonus_length = 8
        self.bonus_points = 20
        self.hand_size = 8
        self.goal = 1

        self.turn = None
        self.draw_pile = None
        self.discards = []
        self.left = self.seats[0]
        self.right = self.seats[1]
        self.left.data.side = LEFT
        self.left.data.curr_score = 0
        self.left.data.overall_score = 0
        self.left.data.hand = None
        self.left.data.expeditions = []
        self.right.data.side = RIGHT
        self.right.data.curr_score = 0
        self.right.data.overall_score = 0
        self.right.data.hand = None
        self.right.data.expeditions = []
        self.resigner = None
        self.first_player = None
        self.just_discarded_to = None

        self.printable_layout = None
        self.init_hand()

    def init_hand(self):

        # Depending on the number of suits requested for play, we build
        # structures.
        suit_list = DEFAULT_SUITS[:]
        if self.suit_count >= 6:
            suit_list.append(CYAN)
        if self.suit_count == 7:
            suit_list.append(MAGENTA)

        # If, on the other hand, the number of suits is /less/ than five,
        # we use a subset of the default suits.
        if self.suit_count < 5:
            suit_list = DEFAULT_SUITS[:self.suit_count]

        # All right, we have a list of suits involved in this game.  Let's
        # build the various piles that are based on those suits.
        self.left.data.expeditions = []
        self.right.data.expeditions = []
        self.discards = []
        for suit in suit_list:
            discard_pile = Struct()
            left_expedition = Struct()
            right_expedition = Struct()
            for pile in (discard_pile, left_expedition, right_expedition):
                pile.suit = suit
                pile.hand = Hand()
                pile.value = 0
            self.left.data.expeditions.append(left_expedition)
            self.right.data.expeditions.append(right_expedition)
            self.discards.append(discard_pile)

        # We'll do a separate loop for generating the deck to minimize
        # confusion.
        self.draw_pile = Hand()
        for suit in suit_list:
            for rank in NUMERICAL_RANKS:
                self.draw_pile.add(ExpeditionsCard(rank, suit))

            # Add as many agreements as requested.
            for agreement in range(self.agreement_count):
                self.draw_pile.add(ExpeditionsCard(AGREEMENT, suit))

        # Lastly, shuffle the draw deck and initialize hands.
        self.draw_pile.shuffle()
        self.left.data.hand = Hand()
        self.right.data.hand = Hand()

    def get_discard_str(self, pos):

        discard_pile = self.discards[pos]
        if len(discard_pile.hand):
            return(value_to_str(discard_pile.hand[-1].value()))
        return "."

    def get_expedition_str(self, expedition):

        to_return = ""
        for card in expedition:
            to_return += value_to_str(card.value())

        return to_return

    def get_sp_str(self, seat):

        if seat == self.left:
            return "^C%s^~" % self.left.player_name
        else:
            return "^M%s^~" % self.right.player_name

    def update_printable_layout(self):

        self.printable_layout = []
        self.printable_layout.append("                   .---.\n")

        # Loop through all table rows.
        for row in range(self.suit_count):
            left = self.left.data.expeditions[row]
            right = self.right.data.expeditions[row]
            suit_char = left.suit[0].upper()
            left_suit_char = suit_char
            right_suit_char = suit_char
            expedition_str = get_color_code(left.suit)
            expedition_str += self.get_expedition_str(left.hand.reversed()).rjust(18)
            if self.bonus and len(left.hand) >= self.bonus_length:
                left_suit_char = "*"
            if self.bonus and len(right.hand) >= self.bonus_length:
                right_suit_char = "*"
            expedition_str += " %s %s %s " % (left_suit_char, self.get_discard_str(row), right_suit_char)
            expedition_str += self.get_expedition_str(right.hand)
            expedition_str += "^~\n"
            self.printable_layout.append(expedition_str)
            self.printable_layout.append("                   |   |\n")

        # Replace the last unnecessary separator row with the end of the board.
        self.printable_layout[-1] = "                   `---'\n"

    def get_metadata_str(self):

        to_return = "^Y%s^~ remain in the draw pile.\n" % get_plural_str(len(self.draw_pile), "card")
        if not self.turn:
            to_return += "The game has not started yet.\n"
        else:
            to_return += "It is %s's turn to " % self.get_sp_str(self.turn)
            sub = self.state.get_sub()
            if sub == "play":
                to_return += "^cplay a card^~.\n"
            else:
                to_return += "^cdraw a card^~.\n"
        to_return += "The goal score for this game is ^Y%s^~.\n" % get_plural_str(self.goal, "point")
        to_return += "Overall:      %s: %s     %s: %s\n" % (self.get_sp_str(self.left), self.left.data.overall_score, self.get_sp_str(self.right), self.right.data.overall_score)

        return to_return

    def show(self, player, show_metadata=True):

        if not self.printable_layout:
            self.update_printable_layout()
        player.tell_cc("%s         %s\n" % (self.get_sp_str(self.left).rjust(21), self.get_sp_str(self.right)))
        for line in self.printable_layout:
            player.tell_cc(line)
        if show_metadata:
            player.tell_cc("\n" + self.get_metadata_str())

    def show_hand(self, player):

        seat = self.get_seat_of_player(player)

        if not seat:
            self.tell_pre(player, "You're not playing!\n")
            return

        print_str = "Your current hand:\n   "
        print_str += hand_to_str(seat.data.hand)
        print_str += "\n"
        self.tell_pre(player, print_str)

    def send_layout(self, show_metadata=True):

        for player in self.channel.listeners:
            self.show(player, show_metadata)
        for seat in self.seats:
            if seat.player:
                self.show_hand(seat.player)

    def deal(self):

        # Deal cards until each player has hand_size cards.
        self.bc_pre("A fresh hand is dealt to both players.\n")
        for i in range(self.hand_size):
            self.left.data.hand.add(self.draw_pile.discard())
            self.right.data.hand.add(self.draw_pile.discard())

        # Sort hands.
        self.left.data.hand = sorted_hand(self.left.data.hand)
        self.right.data.hand = sorted_hand(self.right.data.hand)

        # Clear scores.
        self.left.data.curr_score = 0
        self.right.data.curr_score = 0

    def tick(self):

        # If both seats are full and the game is active, autostart.
        if (self.state.get() == "need_players" and self.seats[0].player
           and self.seats[1].player and self.active):
            self.state.set("playing")
            self.state.set_sub("play")
            self.bc_pre("^CLeft^~: ^Y%s^~; ^MRight^~: ^Y%s^~\n" %
               (self.left.player_name, self.right.player_name))
            self.turn = self.left
            self.first_player = self.left
            self.deal()
            self.send_layout()

    def calculate_deck_size(self, suits, agrees):

        # Eventually this will depend on just what cards are in the deck,
        # but for now there are 9 point cards per suit plus the agreements.
        return (9 + agrees) * suits

    def set_suits(self, player, suit_str):

        if not suit_str.isdigit():
            self.tell_pre(player, "You didn't even send a number!\n")
            return False

        new_suit_count = int(suit_str)
        if new_suit_count < MIN_SUITS or new_suit_count > MAX_SUITS:
            self.tell_pre(player, "The number of suits must be between %d and %d inclusive.\n" % (MIN_SUITS, MAX_SUITS))
            return False

        # Does this give too few cards for the hand size?
        if self.calculate_deck_size(new_suit_count, self.agreement_count) <= self.hand_size * 2:
            self.tell_pre(player, "That number of suits is too small for the hand size.\n")
            return False

        # Valid.
        self.suit_count = new_suit_count
        self.bc_pre("^M%s^~ has changed the suit count to ^G%s^~.\n" % (player, new_suit_count))
        self.init_hand()
        self.update_printable_layout()

    def set_agreements(self, player, agree_str):

        if not agree_str.isdigit():
            self.tell_pre(player, "You didn't even send a number!\n")
            return False

        new_agree_count = int(agree_str)
        if new_agree_count < MIN_AGREEMENTS or new_agree_count > MAX_AGREEMENTS:
            self.tell_pre(player, "The number of agreements must be between %d and %d inclusive.\n" % (MIN_AGREEMENTS, MAX_AGREEMENTS))
            return False

        # Does this give too few cards for the hand size?
        if self.calculate_deck_size(self.suit_count, new_agree_count) <= self.hand_size * 2:
            self.tell_pre(player, "That number of agreements is too small for the hand size.\n")
            return False

        # Valid.
        self.agreement_count = new_agree_count
        self.bc_pre("^M%s^~ has changed the agreement count to ^G%s^~.\n" % (player, new_agree_count))
        self.init_hand()
        self.update_printable_layout()

    def set_hand(self, player, hand_str):

        if not hand_str.isdigit():
            self.tell_pre(player, "You didn't even send a number!\n")
            return False

        new_hand_size = int(hand_str)
        if new_hand_size < MIN_HAND_SIZE or new_hand_size > MAX_HAND_SIZE:
            self.tell_pre(player, "The hand size must be between %d and %d inclusive.\n" % (MIN_HAND_SIZE, MAX_HAND_SIZE))
            return False

        # If the drawn hands are greater than or equal to the actual card
        # count, that doesn't work either.
        if (new_hand_size * 2) >= len(self.draw_pile):
            self.tell_pre(player, "The hand size is too large for the number of cards in play.\n")
            return False

        # Valid.
        self.hand_size = new_hand_size
        self.bc_pre("^M%s^~ has changed the hand size to ^G%s^~.\n" % (player, new_hand_size))

    def set_penalty(self, player, penalty_str):

        if not penalty_str.isdigit():
            self.tell_pre(player, "You didn't even send a number!\n")
            return False

        new_penalty = int(penalty_str)
        if new_penalty < MIN_PENALTY or new_penalty > MAX_PENALTY:
            self.tell_pre(player, "The penalty must be between %d and %d inclusive.\n" % (MIN_PENALTY, MAX_PENALTY))
            return False

        # Valid.
        self.penalty = new_penalty
        self.bc_pre("^M%s^~ has changed the penalty to ^G%s^~.\n" % (player, new_penalty))

    def set_bonus(self, player, bonus_bits):

        if len(bonus_bits) == 1:

            bonus = bonus_bits[0]
            # Gotta be 'none' or 0.
            if bonus in ("none", "n", "0",):
                self.bonus = False
                self.bc_pre("^M%s^~ has disabled the expedition bonuses.\n" % player)
                return True
            else:
                self.tell_pre(player, "Invalid bonus command.\n")
                return False

        elif len(bonus_bits) == 2:

            points, length = bonus_bits

            if not points.isdigit() or not length.isdigit():
                self.tell_pre(player, "Invalid bonus command.\n")
                return False

            points = int(points)
            length = int(length)

            if not points or not length:
                self.bonus = False
                self.bc_pre("^M%s^~ has disabled the expedition bonuses.\n" % player)
                return True
            else:
                self.bonus = True
                self.bonus_points = points
                self.bonus_length = length
                self.bc_pre("^M%s^~ has set the expedition bonuses to ^C%s^~ at length ^R%s^~.\n" % (player, get_plural_str(points, "point"), length))
                return True

        else:
            self.tell_pre(player, "Invalid bonus command.\n")
            return False

    def set_goal(self, player, goal_str):

        if not goal_str.isdigit():
            self.tell_pre(player, "You didn't even send a number!\n")
            return False

        new_goal = int(goal_str)
        if new_goal < 1:
            self.tell_pre(player, "The goal must be at least one point.\n")
            return False

        # Got a valid goal.
        self.goal = new_goal
        self.bc_pre("^M%s^~ has changed the goal to ^G%s^~.\n" % (player, get_plural_str(new_goal, "point")))

    def suit_to_loc(self, suit):

        if suit in DEFAULT_SUITS:
            return DEFAULT_SUITS.index(suit)
        elif suit == CYAN:
            return 5
        elif suit == MAGENTA:
            return 6

        return None

    def evaluate(self, player):

        for seat in self.seats:
            score_str = "%s: " % seat.player_name
            score_str += " + ".join(["%s%s^~" % (get_color_code(x.suit), x.value) for x in seat.data.expeditions])
            score_str += " = %s\n" % (get_plural_str(seat.data.curr_score, "point"))
            self.tell_pre(player, score_str)

    def play(self, player, play_str):

        seat = self.get_seat_of_player(player)
        if not seat:
            self.tell_pre(player, "You're not playing!\n")
            return False

        elif seat != self.turn:
            self.tell_pre(player, "It's not your turn!\n")
            return False

        substate = self.state.get_sub()
        if substate != "play":
            self.tell_pre(player, "You should be drawing or retrieving, not playing!\n")
            return False

        # Translate the play string into an actual card.
        potential_card = str_to_card(play_str)

        if not potential_card:
            self.tell_pre(player, "That's not a valid card!\n")
            return False

        # Do they even have this card?
        if potential_card not in seat.data.hand:
            self.tell_pre(player, "You don't have that card!\n")
            return False

        # All right.  Grab the hand for that expedition.
        exp_hand = seat.data.expeditions[self.suit_to_loc(potential_card.suit)].hand

        # If this card is a lower value than the top card of the hand, nope.
        if len(exp_hand) and potential_card < exp_hand[-1]:
            self.tell_pre(player, "You can no longer play this card on this expedition.\n")
            return False

        # If it's the same value and not an agreement, nope.
        elif (len(exp_hand) and potential_card == exp_hand[-1] and
           potential_card.rank != AGREEMENT):
            self.tell_pre(player, "You cannot play same-valued point cards on an expedition.\n")
            return False

        # Passed the tests.  Play it and clear the discard tracker.
        exp_hand.add(seat.data.hand.discard_specific(potential_card))
        self.just_discarded_to = None

        self.bc_pre("%s played %s.\n" % (self.get_sp_str(seat),
                       card_to_str(potential_card, mode=LONG)))
        return True

    def discard(self, player, play_str):

        seat = self.get_seat_of_player(player)
        if not seat:
            self.tell_pre(player, "You're not playing!\n")
            return False

        elif seat != self.turn:
            self.tell_pre(player, "It's not your turn!\n")
            return False

        substate = self.state.get_sub()
        if substate != "play":
            self.tell_pre(player, "You should be drawing or retrieving, not discarding!\n")
            return False

        # Translate the play string into an actual card.
        potential_card = str_to_card(play_str)

        if not potential_card:
            self.tell_pre(player, "That's not a valid card!\n")
            return False

        # Do they even have this card?
        if potential_card not in seat.data.hand:
            self.tell_pre(player, "You don't have that card!\n")
            return False

        # All right, they can discard it.  Get the appropriate discard pile...
        discard_pile = self.discards[self.suit_to_loc(potential_card.suit)].hand

        discard_pile.add(seat.data.hand.discard_specific(potential_card))

        # Note the pile we just discarded to, so the player can't just pick it
        # back up as their next play.
        self.just_discarded_to = potential_card.suit

        self.bc_pre("%s discarded %s.\n" % (self.get_sp_str(seat),
                          card_to_str(potential_card, mode=LONG)))
        return True

    def draw(self, player):

        seat = self.get_seat_of_player(player)
        if not seat:
            self.tell_pre(player, "You're not playing!\n")
            return False

        elif seat != self.turn:
            self.tell_pre(player, "It's not your turn!\n")
            return False

        substate = self.state.get_sub()
        if substate != "draw":
            self.tell_pre(player, "You should be playing or discarding, not drawing!\n")
            return False

        # Draw a card.  This one's easy!
        draw_card = self.draw_pile.discard()
        seat.data.hand.add(draw_card)

        # Resort the hand.
        seat.data.hand = sorted_hand(seat.data.hand)

        self.bc_pre("%s drew a card.\n" % (self.get_sp_str(seat)))
        self.tell_pre(player, "You drew %s.\n" % card_to_str(draw_card, mode=LONG))
        return True

    def retrieve(self, player, retrieve_str):

        seat = self.get_seat_of_player(player)
        if not seat:
            self.tell_pre(player, "You're not playing!\n")
            return False

        elif seat != self.turn:
            self.tell_pre(player, "It's not your turn!\n")
            return False

        substate = self.state.get_sub()
        if substate != "draw":
            self.tell_pre(player, "You should be playing or discarding, not retrieving!\n")
            return False

        # Turn the retrieve string into an actual suit.
        suit = str_to_suit(retrieve_str)
        if not suit:
            self.tell_pre(player, "That's not a valid suit!\n")
            return False

        # Is that a valid location in this game?
        loc = self.suit_to_loc(suit)
        if loc >= self.suit_count:
            self.tell_pre(player, "That suit isn't in play this game.\n")
            return False

        # Is there actually a card there /to/ draw?
        discard_pile = self.discards[loc].hand
        if not len(discard_pile):
            self.tell_pre(player, "There are no discards of that suit.\n")
            return False

        # Is it the card they just discarded?
        if suit == self.just_discarded_to:
            self.tell_pre(player, "You just discarded that card!\n")
            return False

        # Phew.  All tests passed.  Give them the card.
        dis_card = discard_pile.discard()
        seat.data.hand.add(dis_card)
        seat.data.hand = sorted_hand(seat.data.hand)

        self.bc_pre("%s retrieved %s from the discards.\n" % (self.get_sp_str(seat),
                                                  card_to_str(dis_card, mode=LONG)))
        return True

    def resign(self, player):

        seat = self.get_seat_of_player(player)
        if not seat:
            self.tell_pre(player, "You can't resign; you're not playing!\n")
            return False

        if self.turn != seat:
            self.tell_pre(player, "You must wait for your turn to resign.\n")
            return False

        self.resigner = seat
        self.bc_pre("%s is resigning from the game.\n" % self.get_sp_str(seat))
        return True

    def handle(self, player, command_str):

        # Handle common commands.
        handled = self.handle_common_commands(player, command_str)

        if not handled:

            state = self.state.get()
            command_bits = command_str.lower().split()
            primary = command_bits[0]

            if state == "setup":

                if primary in ("suits",):

                    if len(command_bits) == 2:
                        self.set_suits(player, command_bits[1])
                    else:
                        self.tell_pre(player, "Invalid suits command.\n")
                    handled = True

                elif primary in ("agreements", "agree",):

                    if len(command_bits) == 2:
                        self.set_agreements(player, command_bits[1])
                    else:
                        self.tell_pre(player, "Invalid agree command.\n")
                    handled = True

                elif primary in ("hand",):

                    if len(command_bits) == 2:
                        self.set_hand(player, command_bits[1])
                    else:
                        self.tell_pre(player, "Invalid hand command.\n")
                    handled = True

                elif primary in ("penalty",):

                    if len(command_bits) == 2:
                        self.set_penalty(player, command_bits[1])
                    else:
                        self.tell_pre(player, "Invalid penalty command.\n")
                    handled = True

                elif primary in ("bonus",):

                    if len(command_bits) >= 2:
                        self.set_bonus(player, command_bits[1:])
                    else:
                        self.tell_pre(player, "Invalid bonus command.\n")
                    handled = True

                elif primary in ("goal", "score",):

                    if len(command_bits) == 2:
                        self.set_goal(player, command_bits[1])
                    else:
                        self.tell_pre(player, "Invalid goal command.\n")
                    handled = True

                elif primary in ("done", "ready", "d", "r",):

                    self.bc_pre("The game is now looking for players.\n")
                    self.state.set("need_players")
                    handled = True

            elif state == "need_players":

                if primary in ("config", "setup", "conf",):

                    self.state.set("setup")
                    self.bc_pre("^R%s^~ has switched the game to setup mode.\n" % player)
                    handled = True

            elif state == "playing":

                made_move = False

                if primary in ("hand", "inventory", "inv", "i",):
                    self.show_hand(player)
                    handled = True

                elif primary in ("evaluate", "eval", "score", "e", "s",):
                    self.evaluate(player)
                    handled = True

                elif primary in ("move", "play", "mv", "pl",):

                    if len(command_bits) == 2:
                        made_move = self.play(player, command_bits[1])
                    else:
                        self.tell_pre(player, "Invalid play command.\n")
                    handled = True

                elif primary in ("discard", "toss", "dc", "di", "to",):
                    if len(command_bits) == 2:
                        made_move = self.discard(player, command_bits[1])
                    else:
                        self.tell_pre(player, "Invalid discard command.\n")
                    handled = True

                elif primary in ("draw", "dr",):
                    made_move = self.draw(player)
                    handled = True

                elif primary in ("retrieve", "re",):
                    if len(command_bits) == 2:
                        made_move = self.retrieve(player, command_bits[1])
                    else:
                        self.tell_pre(player, "Invalid retrieve command.\n")
                    handled = True

                elif primary in ("resign",):

                    if self.resign(player):
                        made_move = True
                    handled = True

                if made_move:

                    substate = self.state.get_sub()

                    # Okay, something happened on the layout.  Update scores
                    # and the layout.
                    self.update_scores()
                    self.update_printable_layout()

                    # Is the game over?
                    if not len(self.draw_pile) or self.resigner:

                        # Yup.  Resolve the game.
                        self.resolve_hand()

                        # Is there an overall winner?
                        winner = self.find_winner()

                        if winner:
                            self.resolve(winner)
                            self.finish()

                        else:

                            # Hand over, but not the game itself.  New deal.
                            self.bc_pre("The cards are collected for another hand.\n")
                            self.init_hand()

                            # Switch dealers.
                            self.first_player = self.next_seat(self.first_player)
                            self.turn = self.first_player
                            self.state.set("playing")
                            self.state.set_sub("play")
                            self.deal()
                            self.update_printable_layout()
                            self.send_layout()

                    else:

                        # If we're in the play substate, switch to the draw.
                        if substate == "play":
                            self.state.set_sub("draw")

                        else:

                            # After draw, switch turns and resend the board.
                            self.state.set_sub("play")
                            self.turn = self.next_seat(self.turn)
                            self.send_layout(show_metadata=False)

        if not handled:
            self.tell_pre(player, "Invalid command.\n")

    def update_scores(self):

        for seat in self.left, self.right:

            # Start at 0.
            total = 0

            # For each expedition, if they're even on it...
            for exp in seat.data.expeditions:

                curr = 0
                multiplier = 1

                if len(exp.hand):

                    # Immediately assign the penalty.
                    curr -= self.penalty

                    # Now loop through the cards.
                    for card in exp.hand:

                        value = card.value()
                        if value == 1:

                            # Agreement; adjust multiplier.
                            multiplier += 1

                        else:

                            # Scoring card; increase current score.
                            curr += value

                    # Adjust the current score by the multiplier.
                    curr *= multiplier

                    # If bonuses are active, and this meets it, add it.
                    if self.bonus and len(exp.hand) >= self.bonus_length:
                        curr += self.bonus_points

                # No matter what, add curr to total and set it on the
                # pile.
                total += curr
                exp.value = curr

            # Set the current score for the seat.
            seat.data.curr_score = total

    def resolve_hand(self):

        for seat in self.left, self.right:

            addend = seat.data.curr_score
            if addend > 0:
                adj_str = "^Ygains ^C%s^~" % get_plural_str(addend, "point")
            elif addend < 0:
                adj_str = "^yloses ^c%s^~" % get_plural_str(-addend, "point")
            else:
                adj_str = "^Wsomehow manages to score precisely zero points^~"

            # Actually adjust the scores by the proper amounts, and inform
            # everyone of the result.
            seat.data.overall_score += addend

            # If someone resigned, scores don't matter, so don't show them.
            if not self.resigner:
                self.bc_pre("%s %s, giving them ^G%s^~.\n" % (self.get_sp_str(seat), adj_str, seat.data.overall_score))

    def find_winner(self):

        # If someone resigned, this is the easiest thing ever.
        if self.resigner == self.left:
            return self.right
        elif self.resigner == self.right:
            return self.left

        # If one player has a higher score than the other and that score
        # is higher than the goal, they win.
        if (self.left.data.overall_score > self.right.data.overall_score and
           self.left.data.overall_score >= self.goal):
            return self.left
        elif (self.right.data.overall_score > self.left.data.overall_score and
           self.right.data.overall_score >= self.goal):
            return self.right

        # Either we haven't reached the goal or there's a tie.  We'll print a
        # special message if there's a tie, because that's kinda crazy.
        if self.left.data.overall_score == self.right.data.overall_score:
            self.bc_pre("The players are tied!\n")

        # No matter what, there's no winner.
        return None

    def resolve(self, winner):
        self.bc_pre("%s wins!\n" % self.get_sp_str(winner))

    def show_help(self, player):

        super(Expeditions, self).show_help(player)
        player.tell_cc("\nEXPEDITIONS SETUP PHASE:\n\n")
        player.tell_cc("          ^!setup^., ^!config^., ^!conf^.     Enter setup phase.\n")
        player.tell_cc("                  ^!suits^. <num>     Play with <num> suits.\n")
        player.tell_cc("                  ^!agree^. <num>     Suits have <num> agreements.\n")
        player.tell_cc("                   ^!hand^. <num>     Hands have <num> cards.\n")
        player.tell_cc("                ^!penalty^. <num>     Expeditions start down <num> points.\n")
        player.tell_cc("     ^!bonus^. <pts> <len> | none     Bonus is <pts> at length <len>/none.\n")
        player.tell_cc("            ^!goal^. <num>, ^!score^.     Play until <num> points.\n")
        player.tell_cc("            ^!ready^., ^!done^., ^!r^., ^!d^.     End setup phase.\n")
        player.tell_cc("\nEXPEDITIONS PLAY:\n\n")
        player.tell_cc("              ^!play^. <card>, ^!pl^.     Play <card> from your hand.\n")
        player.tell_cc("         ^!discard^. <card>, ^!toss^.     Discard <card> from your hand.\n")
        player.tell_cc("                     ^!draw^., ^!dr^.     Draw from the draw pile.\n")
        player.tell_cc("          ^!retrieve^. <suit>, ^!re^.     Retrieve top discard of <suit>.\n")
        player.tell_cc("                       ^!resign^.     Resign.\n")
        player.tell_cc("                 ^!hand^., ^!inv^., ^!i^.     Look at the cards in your hand.\n")
        player.tell_cc("               ^!evaluate^., ^!eval^.     Evaluate the current scores.\n")
Пример #2
0
class Expeditions(SeatedGame):
    """A Expeditions game table implementation.  Based on a game invented in
    1999 by Reiner Knizia.
    """

    def __init__(self, server, table_name):

        super(Expeditions, self).__init__(server, table_name)

        self.game_display_name = "Expeditions"
        self.game_name = "expeditions"
        self.seats = [
            Seat("Left"),
            Seat("Right")
        ]
        self.min_players = 2
        self.max_players = 2
        self.state = State("need_players")
        self.prefix = "(^RExpeditions^~): "
        self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name)

        # Expeditions-specific stuff.
        self.suit_count = 5
        self.agreement_count = 3
        self.penalty = 20
        self.bonus = True
        self.bonus_length = 8
        self.bonus_points = 20
        self.hand_size = 8
        self.goal = 1

        self.turn = None
        self.draw_pile = None
        self.discards = []
        self.left = self.seats[0]
        self.right = self.seats[1]
        self.left.data.side = LEFT
        self.left.data.curr_score = 0
        self.left.data.overall_score = 0
        self.left.data.hand = None
        self.left.data.expeditions = []
        self.right.data.side = RIGHT
        self.right.data.curr_score = 0
        self.right.data.overall_score = 0
        self.right.data.hand = None
        self.right.data.expeditions = []
        self.resigner = None
        self.first_player = None
        self.just_discarded_to = None

        self.printable_layout = None
        self.init_hand()

    def init_hand(self):

        # Depending on the number of suits requested for play, we build
        # structures.
        suit_list = DEFAULT_SUITS[:]
        if self.suit_count >= 6:
            suit_list.append(CYAN)
        if self.suit_count == 7:
            suit_list.append(MAGENTA)

        # If, on the other hand, the number of suits is /less/ than five,
        # we use a subset of the default suits.
        if self.suit_count < 5:
            suit_list = DEFAULT_SUITS[:self.suit_count]

        # All right, we have a list of suits involved in this game.  Let's
        # build the various piles that are based on those suits.
        self.left.data.expeditions = []
        self.right.data.expeditions = []
        self.discards = []
        for suit in suit_list:
            discard_pile = Struct()
            left_expedition = Struct()
            right_expedition = Struct()
            for pile in (discard_pile, left_expedition, right_expedition):
                pile.suit = suit
                pile.hand = Hand()
                pile.value = 0
            self.left.data.expeditions.append(left_expedition)
            self.right.data.expeditions.append(right_expedition)
            self.discards.append(discard_pile)

        # We'll do a separate loop for generating the deck to minimize
        # confusion.
        self.draw_pile = Hand()
        for suit in suit_list:
            for rank in NUMERICAL_RANKS:
                self.draw_pile.add(ExpeditionsCard(rank, suit))

            # Add as many agreements as requested.
            for agreement in range(self.agreement_count):
                self.draw_pile.add(ExpeditionsCard(AGREEMENT, suit))

        # Lastly, shuffle the draw deck and initialize hands.
        self.draw_pile.shuffle()
        self.left.data.hand = Hand()
        self.right.data.hand = Hand()

    def get_discard_str(self, pos):

        discard_pile = self.discards[pos]
        if len(discard_pile.hand):
            return(value_to_str(discard_pile.hand[-1].value()))
        return "."

    def get_expedition_str(self, expedition):

        to_return = ""
        for card in expedition:
            to_return += value_to_str(card.value())

        return to_return

    def get_sp_str(self, seat):

        if seat == self.left:
            return "^C%s^~" % self.left.player_name
        else:
            return "^M%s^~" % self.right.player_name

    def update_printable_layout(self):

        self.printable_layout = []
        self.printable_layout.append("                   .---.\n")

        # Loop through all table rows.
        for row in range(self.suit_count):
            left = self.left.data.expeditions[row]
            right = self.right.data.expeditions[row]
            suit_char = left.suit[0].upper()
            left_suit_char = suit_char
            right_suit_char = suit_char
            expedition_str = get_color_code(left.suit)
            expedition_str += self.get_expedition_str(left.hand.reversed()).rjust(18)
            if self.bonus and len(left.hand) >= self.bonus_length:
                left_suit_char = "*"
            if self.bonus and len(right.hand) >= self.bonus_length:
                right_suit_char = "*"
            expedition_str += " %s %s %s " % (left_suit_char, self.get_discard_str(row), right_suit_char)
            expedition_str += self.get_expedition_str(right.hand)
            expedition_str += "^~\n"
            self.printable_layout.append(expedition_str)
            self.printable_layout.append("                   |   |\n")

        # Replace the last unnecessary separator row with the end of the board.
        self.printable_layout[-1] = "                   `---'\n"

    def get_metadata_str(self):

        to_return = "^Y%s^~ remain in the draw pile.\n" % get_plural_str(len(self.draw_pile), "card")
        if not self.turn:
            to_return += "The game has not started yet.\n"
        else:
            to_return += "It is %s's turn to " % self.get_sp_str(self.turn)
            sub = self.state.get_sub()
            if sub == "play":
                to_return += "^cplay a card^~.\n"
            else:
                to_return += "^cdraw a card^~.\n"
        to_return += "The goal score for this game is ^Y%s^~.\n" % get_plural_str(self.goal, "point")
        to_return += "Overall:      %s: %s     %s: %s\n" % (self.get_sp_str(self.left), self.left.data.overall_score, self.get_sp_str(self.right), self.right.data.overall_score)

        return to_return

    def show(self, player, show_metadata=True):

        if not self.printable_layout:
            self.update_printable_layout()
        player.tell_cc("%s         %s\n" % (self.get_sp_str(self.left).rjust(21), self.get_sp_str(self.right)))
        for line in self.printable_layout:
            player.tell_cc(line)
        if show_metadata:
            player.tell_cc("\n" + self.get_metadata_str())

    def show_hand(self, player):

        seat = self.get_seat_of_player(player)

        if not seat:
            self.tell_pre(player, "You're not playing!\n")
            return

        print_str = "Your current hand:\n   "
        print_str += hand_to_str(seat.data.hand)
        print_str += "\n"
        self.tell_pre(player, print_str)

    def send_layout(self, show_metadata=True):

        for player in self.channel.listeners:
            self.show(player, show_metadata)
        for seat in self.seats:
            if seat.player:
                self.show_hand(seat.player)

    def deal(self):

        # Deal cards until each player has hand_size cards.
        self.bc_pre("A fresh hand is dealt to both players.\n")
        for i in range(self.hand_size):
            self.left.data.hand.add(self.draw_pile.discard())
            self.right.data.hand.add(self.draw_pile.discard())

        # Sort hands.
        self.left.data.hand = sorted_hand(self.left.data.hand)
        self.right.data.hand = sorted_hand(self.right.data.hand)

        # Clear scores.
        self.left.data.curr_score = 0
        self.right.data.curr_score = 0

    def tick(self):

        # If both seats are full and the game is active, autostart.
        if (self.state.get() == "need_players" and self.seats[0].player
           and self.seats[1].player and self.active):
            self.state.set("playing")
            self.state.set_sub("play")
            self.bc_pre("^CLeft^~: ^Y%s^~; ^MRight^~: ^Y%s^~\n" %
               (self.left.player_name, self.right.player_name))
            self.turn = self.left
            self.first_player = self.left
            self.deal()
            self.send_layout()

    def calculate_deck_size(self, suits, agrees):

        # Eventually this will depend on just what cards are in the deck,
        # but for now there are 9 point cards per suit plus the agreements.
        return (9 + agrees) * suits

    def set_suits(self, player, suit_str):

        if not suit_str.isdigit():
            self.tell_pre(player, "You didn't even send a number!\n")
            return False

        new_suit_count = int(suit_str)
        if new_suit_count < MIN_SUITS or new_suit_count > MAX_SUITS:
            self.tell_pre(player, "The number of suits must be between %d and %d inclusive.\n" % (MIN_SUITS, MAX_SUITS))
            return False

        # Does this give too few cards for the hand size?
        if self.calculate_deck_size(new_suit_count, self.agreement_count) <= self.hand_size * 2:
            self.tell_pre(player, "That number of suits is too small for the hand size.\n")
            return False

        # Valid.
        self.suit_count = new_suit_count
        self.bc_pre("^M%s^~ has changed the suit count to ^G%s^~.\n" % (player, new_suit_count))
        self.init_hand()
        self.update_printable_layout()

    def set_agreements(self, player, agree_str):

        if not agree_str.isdigit():
            self.tell_pre(player, "You didn't even send a number!\n")
            return False

        new_agree_count = int(agree_str)
        if new_agree_count < MIN_AGREEMENTS or new_agree_count > MAX_AGREEMENTS:
            self.tell_pre(player, "The number of agreements must be between %d and %d inclusive.\n" % (MIN_AGREEMENTS, MAX_AGREEMENTS))
            return False

        # Does this give too few cards for the hand size?
        if self.calculate_deck_size(self.suit_count, new_agree_count) <= self.hand_size * 2:
            self.tell_pre(player, "That number of agreements is too small for the hand size.\n")
            return False

        # Valid.
        self.agreement_count = new_agree_count
        self.bc_pre("^M%s^~ has changed the agreement count to ^G%s^~.\n" % (player, new_agree_count))
        self.init_hand()
        self.update_printable_layout()

    def set_hand(self, player, hand_str):

        if not hand_str.isdigit():
            self.tell_pre(player, "You didn't even send a number!\n")
            return False

        new_hand_size = int(hand_str)
        if new_hand_size < MIN_HAND_SIZE or new_hand_size > MAX_HAND_SIZE:
            self.tell_pre(player, "The hand size must be between %d and %d inclusive.\n" % (MIN_HAND_SIZE, MAX_HAND_SIZE))
            return False

        # If the drawn hands are greater than or equal to the actual card
        # count, that doesn't work either.
        if (new_hand_size * 2) >= len(self.draw_pile):
            self.tell_pre(player, "The hand size is too large for the number of cards in play.\n")
            return False

        # Valid.
        self.hand_size = new_hand_size
        self.bc_pre("^M%s^~ has changed the hand size to ^G%s^~.\n" % (player, new_hand_size))

    def set_penalty(self, player, penalty_str):

        if not penalty_str.isdigit():
            self.tell_pre(player, "You didn't even send a number!\n")
            return False

        new_penalty = int(penalty_str)
        if new_penalty < MIN_PENALTY or new_penalty > MAX_PENALTY:
            self.tell_pre(player, "The penalty must be between %d and %d inclusive.\n" % (MIN_PENALTY, MAX_PENALTY))
            return False

        # Valid.
        self.penalty = new_penalty
        self.bc_pre("^M%s^~ has changed the penalty to ^G%s^~.\n" % (player, new_penalty))

    def set_bonus(self, player, bonus_bits):

        if len(bonus_bits) == 1:

            bonus = bonus_bits[0]
            # Gotta be 'none' or 0.
            if bonus in ("none", "n", "0",):
                self.bonus = False
                self.bc_pre("^M%s^~ has disabled the expedition bonuses.\n" % player)
                return True
            else:
                self.tell_pre(player, "Invalid bonus command.\n")
                return False

        elif len(bonus_bits) == 2:

            points, length = bonus_bits

            if not points.isdigit() or not length.isdigit():
                self.tell_pre(player, "Invalid bonus command.\n")
                return False

            points = int(points)
            length = int(length)

            if not points or not length:
                self.bonus = False
                self.bc_pre("^M%s^~ has disabled the expedition bonuses.\n" % player)
                return True
            else:
                self.bonus = True
                self.bonus_points = points
                self.bonus_length = length
                self.bc_pre("^M%s^~ has set the expedition bonuses to ^C%s^~ at length ^R%s^~.\n" % (player, get_plural_str(points, "point"), length))
                return True

        else:
            self.tell_pre(player, "Invalid bonus command.\n")
            return False

    def set_goal(self, player, goal_str):

        if not goal_str.isdigit():
            self.tell_pre(player, "You didn't even send a number!\n")
            return False

        new_goal = int(goal_str)
        if new_goal < 1:
            self.tell_pre(player, "The goal must be at least one point.\n")
            return False

        # Got a valid goal.
        self.goal = new_goal
        self.bc_pre("^M%s^~ has changed the goal to ^G%s^~.\n" % (player, get_plural_str(new_goal, "point")))

    def suit_to_loc(self, suit):

        if suit in DEFAULT_SUITS:
            return DEFAULT_SUITS.index(suit)
        elif suit == CYAN:
            return 5
        elif suit == MAGENTA:
            return 6

        return None

    def evaluate(self, player):

        for seat in self.seats:
            score_str = "%s: " % seat.player_name
            score_str += " + ".join(["%s%s^~" % (get_color_code(x.suit), x.value) for x in seat.data.expeditions])
            score_str += " = %s\n" % (get_plural_str(seat.data.curr_score, "point"))
            self.tell_pre(player, score_str)

    def play(self, player, play_str):

        seat = self.get_seat_of_player(player)
        if not seat:
            self.tell_pre(player, "You're not playing!\n")
            return False

        elif seat != self.turn:
            self.tell_pre(player, "It's not your turn!\n")
            return False

        substate = self.state.get_sub()
        if substate != "play":
            self.tell_pre(player, "You should be drawing or retrieving, not playing!\n")
            return False

        # Translate the play string into an actual card.
        potential_card = str_to_card(play_str)

        if not potential_card:
            self.tell_pre(player, "That's not a valid card!\n")
            return False

        # Do they even have this card?
        if potential_card not in seat.data.hand:
            self.tell_pre(player, "You don't have that card!\n")
            return False

        # All right.  Grab the hand for that expedition.
        exp_hand = seat.data.expeditions[self.suit_to_loc(potential_card.suit)].hand

        # If this card is a lower value than the top card of the hand, nope.
        if len(exp_hand) and potential_card < exp_hand[-1]:
            self.tell_pre(player, "You can no longer play this card on this expedition.\n")
            return False

        # If it's the same value and not an agreement, nope.
        elif (len(exp_hand) and potential_card == exp_hand[-1] and
           potential_card.rank != AGREEMENT):
            self.tell_pre(player, "You cannot play same-valued point cards on an expedition.\n")
            return False

        # Passed the tests.  Play it and clear the discard tracker.
        exp_hand.add(seat.data.hand.discard_specific(potential_card))
        self.just_discarded_to = None

        self.bc_pre("%s played %s.\n" % (self.get_sp_str(seat),
                       card_to_str(potential_card, mode=LONG)))
        return True

    def discard(self, player, play_str):

        seat = self.get_seat_of_player(player)
        if not seat:
            self.tell_pre(player, "You're not playing!\n")
            return False

        elif seat != self.turn:
            self.tell_pre(player, "It's not your turn!\n")
            return False

        substate = self.state.get_sub()
        if substate != "play":
            self.tell_pre(player, "You should be drawing or retrieving, not discarding!\n")
            return False

        # Translate the play string into an actual card.
        potential_card = str_to_card(play_str)

        if not potential_card:
            self.tell_pre(player, "That's not a valid card!\n")
            return False

        # Do they even have this card?
        if potential_card not in seat.data.hand:
            self.tell_pre(player, "You don't have that card!\n")
            return False

        # All right, they can discard it.  Get the appropriate discard pile...
        discard_pile = self.discards[self.suit_to_loc(potential_card.suit)].hand

        discard_pile.add(seat.data.hand.discard_specific(potential_card))

        # Note the pile we just discarded to, so the player can't just pick it
        # back up as their next play.
        self.just_discarded_to = potential_card.suit

        self.bc_pre("%s discarded %s.\n" % (self.get_sp_str(seat),
                          card_to_str(potential_card, mode=LONG)))
        return True

    def draw(self, player):

        seat = self.get_seat_of_player(player)
        if not seat:
            self.tell_pre(player, "You're not playing!\n")
            return False

        elif seat != self.turn:
            self.tell_pre(player, "It's not your turn!\n")
            return False

        substate = self.state.get_sub()
        if substate != "draw":
            self.tell_pre(player, "You should be playing or discarding, not drawing!\n")
            return False

        # Draw a card.  This one's easy!
        draw_card = self.draw_pile.discard()
        seat.data.hand.add(draw_card)

        # Resort the hand.
        seat.data.hand = sorted_hand(seat.data.hand)

        self.bc_pre("%s drew a card.\n" % (self.get_sp_str(seat)))
        self.tell_pre(player, "You drew %s.\n" % card_to_str(draw_card, mode=LONG))
        return True

    def retrieve(self, player, retrieve_str):

        seat = self.get_seat_of_player(player)
        if not seat:
            self.tell_pre(player, "You're not playing!\n")
            return False

        elif seat != self.turn:
            self.tell_pre(player, "It's not your turn!\n")
            return False

        substate = self.state.get_sub()
        if substate != "draw":
            self.tell_pre(player, "You should be playing or discarding, not retrieving!\n")
            return False

        # Turn the retrieve string into an actual suit.
        suit = str_to_suit(retrieve_str)
        if not suit:
            self.tell_pre(player, "That's not a valid suit!\n")
            return False

        # Is that a valid location in this game?
        loc = self.suit_to_loc(suit)
        if loc >= self.suit_count:
            self.tell_pre(player, "That suit isn't in play this game.\n")
            return False

        # Is there actually a card there /to/ draw?
        discard_pile = self.discards[loc].hand
        if not len(discard_pile):
            self.tell_pre(player, "There are no discards of that suit.\n")
            return False

        # Is it the card they just discarded?
        if suit == self.just_discarded_to:
            self.tell_pre(player, "You just discarded that card!\n")
            return False

        # Phew.  All tests passed.  Give them the card.
        dis_card = discard_pile.discard()
        seat.data.hand.add(dis_card)
        seat.data.hand = sorted_hand(seat.data.hand)

        self.bc_pre("%s retrieved %s from the discards.\n" % (self.get_sp_str(seat),
                                                  card_to_str(dis_card, mode=LONG)))
        return True

    def resign(self, player):

        seat = self.get_seat_of_player(player)
        if not seat:
            self.tell_pre(player, "You can't resign; you're not playing!\n")
            return False

        if self.turn != seat:
            self.tell_pre(player, "You must wait for your turn to resign.\n")
            return False

        self.resigner = seat
        self.bc_pre("%s is resigning from the game.\n" % self.get_sp_str(seat))
        return True

    def handle(self, player, command_str):

        # Handle common commands.
        handled = self.handle_common_commands(player, command_str)

        if not handled:

            state = self.state.get()
            command_bits = command_str.lower().split()
            primary = command_bits[0]

            if state == "setup":

                if primary in ("suits",):

                    if len(command_bits) == 2:
                        self.set_suits(player, command_bits[1])
                    else:
                        self.tell_pre(player, "Invalid suits command.\n")
                    handled = True

                elif primary in ("agreements", "agree",):

                    if len(command_bits) == 2:
                        self.set_agreements(player, command_bits[1])
                    else:
                        self.tell_pre(player, "Invalid agree command.\n")
                    handled = True

                elif primary in ("hand",):

                    if len(command_bits) == 2:
                        self.set_hand(player, command_bits[1])
                    else:
                        self.tell_pre(player, "Invalid hand command.\n")
                    handled = True

                elif primary in ("penalty",):

                    if len(command_bits) == 2:
                        self.set_penalty(player, command_bits[1])
                    else:
                        self.tell_pre(player, "Invalid penalty command.\n")
                    handled = True

                elif primary in ("bonus",):

                    if len(command_bits) >= 2:
                        self.set_bonus(player, command_bits[1:])
                    else:
                        self.tell_pre(player, "Invalid bonus command.\n")
                    handled = True

                elif primary in ("goal", "score",):

                    if len(command_bits) == 2:
                        self.set_goal(player, command_bits[1])
                    else:
                        self.tell_pre(player, "Invalid goal command.\n")
                    handled = True

                elif primary in ("done", "ready", "d", "r",):

                    self.bc_pre("The game is now looking for players.\n")
                    self.state.set("need_players")
                    handled = True

            elif state == "need_players":

                if primary in ("config", "setup", "conf",):

                    self.state.set("setup")
                    self.bc_pre("^R%s^~ has switched the game to setup mode.\n" % player)
                    handled = True

            elif state == "playing":

                made_move = False

                if primary in ("hand", "inventory", "inv", "i",):
                    self.show_hand(player)
                    handled = True

                elif primary in ("evaluate", "eval", "score", "e", "s",):
                    self.evaluate(player)
                    handled = True

                elif primary in ("move", "play", "mv", "pl",):

                    if len(command_bits) == 2:
                        made_move = self.play(player, command_bits[1])
                    else:
                        self.tell_pre(player, "Invalid play command.\n")
                    handled = True

                elif primary in ("discard", "toss", "di", "to",):
                    if len(command_bits) == 2:
                        made_move = self.discard(player, command_bits[1])
                    else:
                        self.tell_pre(player, "Invalid discard command.\n")
                    handled = True

                elif primary in ("draw", "dr",):
                    made_move = self.draw(player)
                    handled = True

                elif primary in ("retrieve", "re",):
                    if len(command_bits) == 2:
                        made_move = self.retrieve(player, command_bits[1])
                    else:
                        self.tell_pre(player, "Invalid retrieve command.\n")
                    handled = True

                elif primary in ("resign",):

                    if self.resign(player):
                        made_move = True
                    handled = True

                if made_move:

                    substate = self.state.get_sub()

                    # Okay, something happened on the layout.  Update scores
                    # and the layout.
                    self.update_scores()
                    self.update_printable_layout()

                    # Is the game over?
                    if not len(self.draw_pile) or self.resigner:

                        # Yup.  Resolve the game.
                        self.resolve_hand()

                        # Is there an overall winner?
                        winner = self.find_winner()

                        if winner:
                            self.resolve(winner)
                            self.finish()

                        else:

                            # Hand over, but not the game itself.  New deal.
                            self.bc_pre("The cards are collected for another hand.\n")
                            self.init_hand()

                            # Switch dealers.
                            self.first_player = self.next_seat(self.first_player)
                            self.turn = self.first_player
                            self.state.set("playing")
                            self.state.set_sub("play")
                            self.deal()
                            self.update_printable_layout()
                            self.send_layout()

                    else:

                        # If we're in the play substate, switch to the draw.
                        if substate == "play":
                            self.state.set_sub("draw")

                        else:

                            # After draw, switch turns and resend the board.
                            self.state.set_sub("play")
                            self.turn = self.next_seat(self.turn)
                            self.send_layout(show_metadata=False)

        if not handled:
            self.tell_pre(player, "Invalid command.\n")

    def update_scores(self):

        for seat in self.left, self.right:

            # Start at 0.
            total = 0

            # For each expedition, if they're even on it...
            for exp in seat.data.expeditions:

                curr = 0
                multiplier = 1

                if len(exp.hand):

                    # Immediately assign the penalty.
                    curr -= self.penalty

                    # Now loop through the cards.
                    for card in exp.hand:

                        value = card.value()
                        if value == 1:

                            # Agreement; adjust multiplier.
                            multiplier += 1

                        else:

                            # Scoring card; increase current score.
                            curr += value

                    # Adjust the current score by the multiplier.
                    curr *= multiplier

                    # If bonuses are active, and this meets it, add it.
                    if self.bonus and len(exp.hand) >= self.bonus_length:
                        curr += self.bonus_points

                # No matter what, add curr to total and set it on the
                # pile.
                total += curr
                exp.value = curr

            # Set the current score for the seat.
            seat.data.curr_score = total

    def resolve_hand(self):

        for seat in self.left, self.right:

            addend = seat.data.curr_score
            if addend > 0:
                adj_str = "^Ygains ^C%s^~" % get_plural_str(addend, "point")
            elif addend < 0:
                adj_str = "^yloses ^c%s^~" % get_plural_str(-addend, "point")
            else:
                adj_str = "^Wsomehow manages to score precisely zero points^~"

            # Actually adjust the scores by the proper amounts, and inform
            # everyone of the result.
            seat.data.overall_score += addend

            # If someone resigned, scores don't matter, so don't show them.
            if not self.resigner:
                self.bc_pre("%s %s, giving them ^G%s^~.\n" % (self.get_sp_str(seat), adj_str, seat.data.overall_score))

    def find_winner(self):

        # If someone resigned, this is the easiest thing ever.
        if self.resigner == self.left:
            return self.right
        elif self.resigner == self.right:
            return self.left

        # If one player has a higher score than the other and that score
        # is higher than the goal, they win.
        if (self.left.data.overall_score > self.right.data.overall_score and
           self.left.data.overall_score >= self.goal):
            return self.left
        elif (self.right.data.overall_score > self.left.data.overall_score and
           self.right.data.overall_score >= self.goal):
            return self.right

        # Either we haven't reached the goal or there's a tie.  We'll print a
        # special message if there's a tie, because that's kinda crazy.
        if self.left.data.overall_score == self.right.data.overall_score:
            self.bc_pre("The players are tied!\n")

        # No matter what, there's no winner.
        return None

    def resolve(self, winner):
        self.bc_pre("%s wins!\n" % self.get_sp_str(winner))

    def show_help(self, player):

        super(Expeditions, self).show_help(player)
        player.tell_cc("\nEXPEDITIONS SETUP PHASE:\n\n")
        player.tell_cc("          ^!setup^., ^!config^., ^!conf^.     Enter setup phase.\n")
        player.tell_cc("                  ^!suits^. <num>     Play with <num> suits.\n")
        player.tell_cc("                  ^!agree^. <num>     Suits have <num> agreements.\n")
        player.tell_cc("                   ^!hand^. <num>     Hands have <num> cards.\n")
        player.tell_cc("                ^!penalty^. <num>     Expeditions start down <num> points.\n")
        player.tell_cc("     ^!bonus^. <pts> <len> | none     Bonus is <pts> at length <len>/none.\n")
        player.tell_cc("            ^!goal^. <num>, ^!score^.     Play until <num> points.\n")
        player.tell_cc("            ^!ready^., ^!done^., ^!r^., ^!d^.     End setup phase.\n")
        player.tell_cc("\nEXPEDITIONS PLAY:\n\n")
        player.tell_cc("              ^!play^. <card>, ^!pl^.     Play <card> from your hand.\n")
        player.tell_cc("         ^!discard^. <card>, ^!toss^.     Discard <card> from your hand.\n")
        player.tell_cc("                     ^!draw^., ^!dr^.     Draw from the draw pile.\n")
        player.tell_cc("          ^!retrieve^. <suit>, ^!re^.     Retrieve top discard of <suit>.\n")
        player.tell_cc("                       ^!resign^.     Resign.\n")
        player.tell_cc("                 ^!hand^., ^!inv^., ^!i^.     Look at the cards in your hand.\n")
        player.tell_cc("               ^!evaluate^., ^!eval^.     Evaluate the current scores.\n")