def __init__(self, server, table_name): super(Hokm, self).__init__(server, table_name) self.game_display_name = "Hokm" self.game_name = "hokm" self.state = State("need_players") self.prefix = "(^RHokm^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Hokm-specific stuff. self.goal = 7 self.trick = None self.trump_suit = None self.led_suit = None self.turn = None self.dealer = None self.hakem = None self.winner = None # Default to four-player mode. self.mode = 4 self.short = True self.setup_mode()
def __init__(self, server, table_name): super(Metamorphosis, self).__init__(server, table_name) self.game_display_name = "Metamorphosis" self.game_name = "metamorphosis" self.seats = [Seat("Black"), Seat("White")] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.prefix = "(^RMetamorphosis^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Metamorphosis-specific stuff. self.board = None self.printable_board = None self.size = 12 self.ko_fight = True self.group_count = None self.turn = None self.turn_number = 0 self.seats[0].data.side = BLACK self.seats[0].data.last_was_ko = False self.seats[1].data.side = WHITE self.seats[1].data.last_was_ko = False self.last_r = None self.last_c = None self.resigner = None self.adjacency_map = None self.found_winner = False self.init_board()
def __init__(self, server, table_name): super(Breakthrough, self).__init__(server, table_name) self.game_display_name = "Breakthrough" self.game_name = "breakthrough" self.seats = [Seat("Black"), Seat("White")] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.prefix = "(^RBreakthrough^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Breakthrough-specific stuff. self.width = 8 self.height = 8 self.turn = None self.black = self.seats[0] self.white = self.seats[1] self.resigner = None self.layout = None # We cheat and create a black and white piece here. Breakthrough # doesn't differentiate between pieces, so this allows us to do # comparisons later. self.bp = Piece("^K", "b", "B") self.bp.data.owner = self.black self.wp = Piece("^W", "w", "W") self.wp.data.owner = self.white self.init_board()
def __init__(self, server, table_name): super(SquareOust, self).__init__(server, table_name) self.game_display_name = "Square Oust" self.game_name = "square_oust" self.seats = [ Seat("Black"), Seat("White"), ] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.prefix = "(^RSquare Oust^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Square Oust-specific stuff. self.height = 11 self.width = 11 self.turn = None self.black = self.seats[0] self.black.data.seat_str = "^KBlack^~" self.black.data.groups = [] self.black.data.made_move = False self.white = self.seats[1] self.white.data.seat_str = "^WWhite^~" self.white.data.groups = [] self.white.data.made_move = False self.resigner = None self.layout = None self.move_was_capture = False # Initialize the starting layout. self.init_layout()
def __init__(self, server, table_name): super(Gonnect, self).__init__(server, table_name) self.game_display_name = "Gonnect" self.game_name = "gonnect" self.seats = [ Seat("Black"), Seat("White") ] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.prefix = "(^RGonnect^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Gonnect-specific stuff. self.turn = None self.seats[0].data.side = BLACK self.seats[0].data.dir_str = "/Vertical" self.seats[1].data.side = WHITE self.seats[1].data.dir_str = "/Horizontal" self.directional = False self.resigner = None self.turn_number = 0 self.goban = giles.games.goban.Goban() self.adjacency_map = None self.found_winner = False # A traditional Gonnect board is 13x13. self.goban.resize(13, 13)
def __init__(self, server, table_name): super(Crossway, self).__init__(server, table_name) self.game_display_name = "Crossway" self.game_name = "crossway" self.seats = [Seat("Black"), Seat("White")] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.prefix = "(^RCrossway^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Crossway-specific stuff. self.board = None self.printable_board = None self.size = 19 self.turn = None self.turn_number = 0 self.seats[0].data.side = BLACK self.seats[1].data.side = WHITE self.last_r = None self.last_c = None self.resigner = None self.adjacency_map = None self.found_winner = False self.init_board()
def __init__(self, server, table_name): super(Tanbo, self).__init__(server, table_name) self.game_display_name = "Tanbo" self.game_name = "tanbo" self.seats = [ Seat("Black"), Seat("White"), ] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.prefix = "(^RTanbo^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Tanbo-specific stuff. self.size = 19 self.turn = None self.black = self.seats[0] self.black.data.seat_str = "^KBlack^~" self.black.data.root_list = [] self.white = self.seats[1] self.white.data.seat_str = "^WWhite^~" self.white.data.root_list = [] self.resigner = None self.layout = None # Initialize the starting layout. self.init_layout()
def __init__(self, server, table_name): super(Ataxx, self).__init__(server, table_name) self.game_display_name = "Ataxx" self.game_name = "ataxx" self.seats = [ Seat("Red"), Seat("Blue"), ] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.prefix = "(^RAtaxx^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Ataxx-specific stuff. self.board = None self.printable_board = None self.sides = {} self.size = 7 self.player_mode = 2 self.turn = None self.last_r = None self.last_c = None self.init_seats() self.init_board()
def __init__(self, server, table_name): super(CaptureGo, self).__init__(server, table_name) self.game_display_name = "Capture Go" self.game_name = "capturego" self.seats = [ Seat("Black"), Seat("White") ] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.prefix = "(^RCapture Go^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Capture Go-specific stuff. self.turn = None self.seats[0].data.side = BLACK self.seats[1].data.side = WHITE self.seats[0].data.capture_list = [] self.seats[1].data.capture_list = [] self.capture_goal = 1 self.resigner = None self.turn_number = 0 self.goban = giles.games.goban.Goban()
def __init__(self, server, table_name): super(Talpa, self).__init__(server, table_name) self.game_display_name = "Talpa" self.game_name = "talpa" self.seats = [Seat("Red"), Seat("Blue")] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.prefix = "(^RTalpa^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Talpa-specific stuff. self.size = 8 self.turn = None self.red = self.seats[0] self.red.data.seat_str = "^RRed/Vertical^~" self.blue = self.seats[1] self.blue.data.seat_str = "^BBlue/Horizontal^~" self.resigner = None self.layout = None # Like in most connection games, there is no difference between pieces # of a given color, so we save time and create our singleton pieces # here. self.rp = Piece("^R", "x", "X") self.rp.data.owner = self.red self.bp = Piece("^B", "o", "O") self.bp.data.owner = self.blue # Initialize the starting layout. self.init_layout()
def __init__(self, server, table_name): super(Metamorphosis, self).__init__(server, table_name) self.game_display_name = "Metamorphosis" self.game_name = "metamorphosis" self.seats = [ Seat("Black"), Seat("White") ] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.prefix = "(^RMetamorphosis^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Metamorphosis-specific stuff. self.board = None self.printable_board = None self.size = 12 self.ko_fight = True self.group_count = None self.turn = None self.turn_number = 0 self.seats[0].data.side = BLACK self.seats[0].data.last_was_ko = False self.seats[1].data.side = WHITE self.seats[1].data.last_was_ko = False self.last_r = None self.last_c = None self.resigner = None self.adjacency_map = None self.found_winner = False self.init_board()
def __init__(self, server, table_name): super(Crossway, self).__init__(server, table_name) self.game_display_name = "Crossway" self.game_name = "crossway" self.seats = [ Seat("Black"), Seat("White") ] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.prefix = "(^RCrossway^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Crossway-specific stuff. self.board = None self.printable_board = None self.size = 19 self.turn = None self.turn_number = 0 self.seats[0].data.side = BLACK self.seats[1].data.side = WHITE self.last_r = None self.last_c = None self.resigner = None self.adjacency_map = None self.found_winner = False self.init_board()
def __init__(self, server, table_name): super(Breakthrough, self).__init__(server, table_name) self.game_display_name = "Breakthrough" self.game_name = "breakthrough" self.seats = [ Seat("Black"), Seat("White") ] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.prefix = "(^RBreakthrough^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Breakthrough-specific stuff. self.width = 8 self.height = 8 self.turn = None self.black = self.seats[0] self.white = self.seats[1] self.resigner = None self.layout = None # We cheat and create a black and white piece here. Breakthrough # doesn't differentiate between pieces, so this allows us to do # comparisons later. self.bp = Piece("^K", "b", "B") self.bp.data.owner = self.black self.wp = Piece("^W", "w", "W") self.wp.data.owner = self.white self.init_board()
def __init__(self, server, table_name): super(Poison, self).__init__(server, table_name) self.game_display_name = "Poison" self.game_name = "poison" self.seats = [ Seat("Alpha"), Seat("Bravo"), Seat("Charlie"), Seat("Delta"), Seat("Echo"), Seat("Foxtrot"), Seat("Golf"), Seat("Hotel"), Seat("India"), Seat("Juliet") ] self.state = State("need_players") self.prefix = "(^RPoison^~) " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) self.min_players = 3 self.max_players = 10 # Default configuration. self.antidote_count = 3 self.poison_count = 1 self.goal = 2 # Mutable information. self.turn = None self.highest_bidder = None
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__(self, server, table_name): super(Redstone, self).__init__(server, table_name) self.game_display_name = "Redstone" self.game_name = "redstone" self.seats = [ Seat("Black"), Seat("White"), ] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.prefix = "(^RRedstone^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Redstone-specific stuff. self.height = 19 self.width = 19 self.turn = None self.black = self.seats[0] self.black.data.seat_str = "^KBlack^~" self.black.data.made_move = False self.white = self.seats[1] self.white.data.seat_str = "^WWhite^~" self.white.data.made_move = False self.resigner = None self.layout = None # Like most abstracts, Redstone doesn't need to differentiate between # the pieces on the board. self.bp = Piece("^K", "x", "X") self.bp.data.owner = self.black self.black.data.piece = self.bp self.wp = Piece("^W", "o", "O") self.wp.data.owner = self.white self.white.data.piece = self.wp self.rp = Piece("^R", "r", "R") self.rp.data.owner = None # Initialize the starting layout. self.init_layout()
def __init__(self, server, table_name): super(FortyOne, self).__init__(server, table_name) self.game_display_name = "Forty-One" self.game_name = "fortyone" # Seat ordering is "Persian." self.seats = [ Seat("North"), Seat("West"), Seat("South"), Seat("East"), ] self.min_players = 4 self.max_players = 4 self.state = State("need_players") self.prefix = "(^RForty-One^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # 41-specific stuff. self.goal = 41 self.double = 7 self.minimum = 11 self.whist = False self.positive = True self.trick = None self.trump_suit = None self.led_suit = None self.turn = None self.dealer = None self.winner = None self.north = self.seats[0] self.west = self.seats[1] self.south = self.seats[2] self.east = self.seats[3] self.north.data.who = NORTH self.west.data.who = WEST self.south.data.who = SOUTH self.east.data.who = EAST self.north.data.partner = self.south self.west.data.partner = self.east self.south.data.partner = self.north self.east.data.partner = self.west self.layout = FourPlayerCardGameLayout() # Set everyone's score to zero. for seat in self.seats: seat.data.score = 0
def __init__(self, server, table_name): super(RockPaperScissors, self).__init__(server, table_name) self.game_display_name = "Rock-Paper-Scissors" self.game_name = "rps" self.seats = [ Seat("Left"), Seat("Right"), ] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.plays = [None, None] self.prefix = "(^RRPS^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # RPS requires both seats, so may as well mark them active. self.seats[0].active = True self.seats[1].active = True
def __init__(self, server, table_name): super(Y, self).__init__(server, table_name) self.game_display_name = "Y" self.game_name = "y" self.seats = [ Seat("White"), Seat("Black"), ] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.prefix = "(^RY^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Y-specific guff. self.seats[0].data.color = WHITE self.seats[0].data.color_code = "^W" self.seats[1].data.color = BLACK self.seats[1].data.color_code = "^K" self.board = None self.printable_board = None self.size = 19 self.empty_space_count = None self.master = False self.turn = None self.turn_number = 0 self.move_list = [] self.last_moves = [] self.resigner = None self.adjacency = None self.found_winner = False # Y requires both seats, so may as well mark them active. self.seats[0].active = True self.seats[1].active = True self.init_board()
def __init__(self, server, table_name): super(Whist, self).__init__(server, table_name) self.game_display_name = "Whist" self.game_name = "whist" self.seats = [ Seat("North"), Seat("East"), Seat("South"), Seat("West"), ] self.min_players = 4 self.max_players = 4 self.state = State("need_players") self.prefix = "(^RWhist^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Whist-specific guff. self.ns = Struct() self.ns.score = 0 self.ew = Struct() self.ew.score = 0 self.seats[0].data.who = NORTH self.seats[1].data.who = EAST self.seats[2].data.who = SOUTH self.seats[3].data.who = WEST self.goal = 5 self.trick = None self.trump_suit = None self.led_suit = None self.turn = None self.dealer = None self.winner = None self.layout = FourPlayerCardGameLayout()
def __init__(self, server, table_name): super(Set, self).__init__(server, table_name) self.game_display_name = "Set" self.game_name = "set" self.seats = [] self.min_players = 1 self.max_players = 32767 # We don't even use this. self.state = State("need_players") self.prefix = "(^RSet^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Set-specific stuff. self.max_cards_on_table = DEFAULT_MAX_CARDS self.deal_delay = DEFAULT_DEAL_DELAY self.layout = None self.printable_layout = None self.deck = None self.last_play_time = None self.max_card_count = 81 self.has_borders = True
def __init__(self, server, table_name): super(Talpa, self).__init__(server, table_name) self.game_display_name = "Talpa" self.game_name = "talpa" self.seats = [ Seat("Red"), Seat("Blue"), ] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.prefix = "(^RTalpa^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Talpa-specific stuff. self.size = 8 self.turn = None self.red = self.seats[0] self.red.data.seat_str = "^RRed/Vertical^~" self.blue = self.seats[1] self.blue.data.seat_str = "^BBlue/Horizontal^~" self.resigner = None self.layout = None # Like in most connection games, there is no difference between pieces # of a given color, so we save time and create our singleton pieces # here. self.rp = Piece("^R", "x", "X") self.rp.data.owner = self.red self.bp = Piece("^B", "o", "O") self.bp.data.owner = self.blue # Initialize the starting layout. self.init_layout()
def __init__(self, server, table_name): self.server = server self.channel = server.channel_manager.has_channel(table_name) if not self.channel: self.channel = self.server.channel_manager.add_channel(table_name, gameable=True, persistent=True) else: self.channel.persistent = True self.game_display_name = "Generic Game" self.game_name = "game" self.table_display_name = table_name self.table_name = table_name.lower() self.active = False self.private = False self.state = State("config") self.prefix = "(^RGame^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Override this next variable in your subclasses if you're not # done debugging them. self.debug = False
def __init__(self, server, table_name): super(CaptureGo, self).__init__(server, table_name) self.game_display_name = "Capture Go" self.game_name = "capturego" self.seats = [Seat("Black"), Seat("White")] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.prefix = "(^RCapture Go^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Capture Go-specific stuff. self.turn = None self.seats[0].data.side = BLACK self.seats[1].data.side = WHITE self.seats[0].data.capture_list = [] self.seats[1].data.capture_list = [] self.capture_goal = 1 self.resigner = None self.turn_number = 0 self.goban = giles.games.goban.Goban()
def handle(self, player): state = player.state substate = state.get_sub() if substate == None: # Just logged in. Print the helpful banner. player.tell_cc( "\n\n\n Welcome to ^G%s^~!\n\n" % self.server.name) player.tell_cc("Source URL: ^Y%s^~\n\n" % self.server.source_url) state.set_sub("entry_prompt") elif substate == "entry_prompt": # Ask them for their name and set our state to waiting for an entry. player.tell("\n\nPlease enter your name: ") state.set_sub("name_entry") elif substate == "name_entry": name = player.client.get_command() if name: # Attempt to set their name to the one they requested. is_valid = player.set_name(name) if is_valid: # Welcome them and move them to chat. player.tell("\nWelcome, %s!\n" % player) player.state = State("chat") self.server.log.log("%s logged in from %s." % (player, player.client.addrport())) else: state.set_sub("entry_prompt")
class Hokm(SeatedGame): """A Hokm game table implementation. Hokm is a Persian trick-taking card game of unknown provenance. This implementation doesn't currently rearrange the seats at the start, but does support both the standard 4p partnership game and the quirky 3p mode. In addition, 3p mode can use both a short deck (13 cards per hand) or a long deck (17). """ def __init__(self, server, table_name): super(Hokm, self).__init__(server, table_name) self.game_display_name = "Hokm" self.game_name = "hokm" self.state = State("need_players") self.prefix = "(^RHokm^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Hokm-specific stuff. self.goal = 7 self.trick = None self.trump_suit = None self.led_suit = None self.turn = None self.dealer = None self.hakem = None self.winner = None # Default to four-player mode. self.mode = 4 self.short = True self.setup_mode() def setup_mode(self): # Sets up all of the structures that depend on the mode of Hokm # we're playing: seats, layouts, and (in 4p mode) partnerships. # Remember that seats in Hokm go the opposite direction of the # American/European standard. if self.mode == 4: self.seats = [ Seat("North"), Seat("West"), Seat("South"), Seat("East"), ] self.seats[0].data.who = NORTH self.seats[1].data.who = WEST self.seats[2].data.who = SOUTH self.seats[3].data.who = EAST self.min_players = 4 self.max_players = 4 self.layout = FourPlayerCardGameLayout() # Set up the partnership structures. self.ns = Struct() self.ew = Struct() self.ns.score = 0 self.ew.score = 0 elif self.mode == 3: self.seats = [ Seat("West"), Seat("South"), Seat("East"), ] self.seats[0].data.who = WEST self.seats[1].data.who = SOUTH self.seats[2].data.who = EAST self.west = self.seats[0] self.south = self.seats[1] self.east = self.seats[2] self.west.data.score = 0 self.south.data.score = 0 self.east.data.score = 0 self.min_players = 3 self.max_players = 3 self.layout = ThreePlayerCardGameLayout() else: self.log_pre( "MAJOR ERROR: Hokm initialization with invalid mode %s!" % self.mode) def show_help(self, player): super(Hokm, self).show_help(player) player.tell_cc("\nHOKM SETUP PHASE:\n\n") player.tell_cc( " ^!setup^., ^!config^., ^!conf^. Enter setup phase.\n" ) player.tell_cc( " ^!goal^. <num>, ^!score^. Set the goal score to <num>.\n" ) player.tell_cc( " ^!players^. 3|4, ^!pl^. Set the number of players.\n" ) player.tell_cc( " ^!short^. on|off, ^!sh^. Use a short deck (3p only).\n" ) player.tell_cc( " ^!ready^., ^!done^., ^!r^., ^!d^. End setup phase.\n" ) player.tell_cc("\nHOKM PLAY:\n\n") player.tell_cc( " ^!choose^. <suit>, ^!ch^. Declare <suit> as trumps. Hakem only.\n" ) player.tell_cc( " ^!play^. <card>, ^!pl^. Play <card> from your hand.\n" ) player.tell_cc( " ^!hand^., ^!inv^., ^!i^. Look at the cards in your hand.\n" ) def display(self, player): player.tell_cc("%s" % self.layout) def get_color_code(self, seat): if self.mode == 4: if seat == self.seats[0] or seat == self.seats[2]: return "^R" else: return "^M" else: if seat == self.west: return "^M" elif seat == self.south: return "^R" else: return "^B" def get_sp_str(self, seat): return "^G%s^~ (%s%s^~)" % (seat.player_name, self.get_color_code(seat), seat) def get_score_str(self): if self.mode == 4: return " ^RNorth/South^~: %d ^MEast/West^~: %d\n" % ( self.ns.score, self.ew.score) else: return " ^M%s^~: %d ^R%s^~: %d ^B%s^~: %d\n" % ( self.west.player_name, self.west.data.score, self.south.player_name, self.south.data.score, self.east.player_name, self.east.data.score) def get_metadata(self): to_return = "\n\n" if self.turn: seat_color = self.get_color_code(self.turn) to_return += "%s is the hakem.\n" % (self.get_sp_str(self.hakem)) if self.trump_suit: trump_str = "^C%s^~" % self.trump_suit else: trump_str = "^cwaiting to be chosen^~" to_return += "It is ^Y%s^~'s turn (%s%s^~). Trumps are ^C%s^~.\n" % ( self.turn.player_name, seat_color, self.turn, trump_str) if self.mode == 4: to_return += "Tricks: ^RNorth/South^~: %d ^MEast/West^~: %d\n" % ( self.ns.tricks, self.ew.tricks) else: to_return += "Tricks: ^M%s^~: %d ^R%s^~: %d ^B%s^~: %d\n" % ( self.west.player_name, self.west.data.tricks, self.south.player_name, self.south.data.tricks, self.east.player_name, self.east.data.tricks) to_return += "The goal score for this game is ^C%s^~.\n" % get_plural_str( self.goal, "point") to_return += self.get_score_str() return to_return def show(self, player): self.display(player) player.tell_cc(self.get_metadata()) 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 set_short(self, player, short_bits): if self.mode != 3: self.tell_pre( player, "Cannot set short mode when not in 3-player mode.\n") return False short_bool = booleanize(short_bits) if short_bool: if short_bool > 0: self.short = True display_str = "^Con^~" elif short_bool < 0: self.short = False display_str = "^coff^~" self.bc_pre("^R%s^~ has turned short suits %s.\n" % (player, display_str)) else: self.tell_pre(player, "Not a valid boolean!\n") def set_players(self, player, player_str): if not player_str.isdigit(): self.tell_pre(player, "You didn't even send a number!\n") return False new_mode = int(player_str) if new_mode == self.mode: self.tell_pre(player, "That is the current player count.\n") return False elif new_mode != 3 and new_mode != 4: self.tell_pre(player, "Only 3-player and 4-player Hokm is supported.\n") return False # Got a valid mode. self.mode = new_mode self.bc_pre("^M%s^~ has changed the number of players to ^G%s^~.\n" % (player, new_mode)) self.setup_mode() def clear_trick(self): # Set the current trick to an empty hand... self.trick = Hand() self.led_suit = None # ...and set everyone's played card to None. for seat in self.seats: seat.data.card = None # Clear the layout as well. self.layout.clear() def new_deck(self): # In 4-player mode, it's a standard 52-card pack. if self.mode == 4: self.deck = new_deck() else: # If it's a short deck, 7-A are full; if a long deck, 3-A are. full_ranks = [ACE, KING, QUEEN, JACK, '10', '9', '8', '7', '6'] if self.short: short_rank = '5' else: full_ranks.extend(['5', '4', '3']) short_rank = '2' # Build the deck, full ranks first. self.deck = Hand() for suit in (CLUBS, DIAMONDS, HEARTS, SPADES): for rank in full_ranks: self.deck.add(PlayingCard(rank, suit)) # We only want three of the short rank. No hearts, because. for suit in (CLUBS, DIAMONDS, SPADES): self.deck.add(PlayingCard(short_rank, suit)) def start_deal(self): # Set the trick counts to zero, appropriately for the mode. if self.mode == 4: self.ns.tricks = 0 self.ew.tricks = 0 else: self.west.data.tricks = 0 self.south.data.tricks = 0 self.east.data.tricks = 0 dealer_name = self.dealer.player_name self.bc_pre( "^R%s^~ (%s%s^~) gives the cards a good shuffle...\n" % (dealer_name, self.get_color_code(self.dealer), self.dealer)) self.new_deck() self.deck.shuffle() # Deal out five cards each. self.bc_pre("^R%s^~ deals five cards out to each of the players.\n" % dealer_name) for seat in self.seats: seat.data.hand = Hand() for i in range(5): for seat in self.seats: seat.data.hand.add(self.deck.discard()) # Clear the internal metadata about trumps. self.trump_suit = None # Sort the hakem's hand. self.hakem.data.hand = sorted_hand(self.hakem.data.hand) # Show the hakem their hand. if self.hakem.player: self.tell_pre(self.hakem.player, "Please choose a trump suit for this hand.\n") self.show_hand(self.hakem.player) # The hakem both chooses and, eventually, leads. self.turn = self.hakem self.layout.change_turn(self.hakem.data.who) # Shift into "choosing" mode. self.state.set("choosing") def finish_deal(self): self.bc_pre("^R%s^~ finishes dealing the cards out.\n" % self.dealer.player_name) while len(self.deck): for seat in self.seats: seat.data.hand.add(self.deck.discard()) # Sort everyone's hands now that we have a trump suit. for seat in self.seats: seat.data.hand = sorted_hand(seat.data.hand, self.trump_suit) # Show everyone their completed hands. self.show_hands() # We're playing now. self.state.set("playing") 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, self.trump_suit) print_str += "\n" self.tell_pre(player, print_str) def show_hands(self): for seat in self.seats: if seat.player: self.show_hand(seat.player) 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 # 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 # Okay, it's a card in their hand. First, let's do the "follow the # led suit" business. action_str = "^Wplays^~" if self.led_suit: this_suit = potential_card.suit if (this_suit != self.led_suit and hand_has_suit(seat.data.hand, self.led_suit)): # You can't play off-suit if you can match the led suit. self.tell_pre(player, "You can't throw off; you have the led suit.\n") return False else: # No led suit; they're the leader. action_str = "^Yleads^~ with" self.led_suit = potential_card.suit # They either matched the led suit, didn't have any of it, or they # are themselves the leader. Nevertheless, their play is valid. seat.data.card = potential_card self.trick.add(seat.data.hand.discard_specific(potential_card)) trump_str = "" if potential_card.suit == self.trump_suit: trump_str = ", a ^Rtrump^~" self.bc_pre("%s %s ^C%s^~%s.\n" % (self.get_sp_str(seat), action_str, card_to_str(potential_card, LONG), trump_str)) self.layout.place(seat.data.who, potential_card) return potential_card def tick(self): # If all seats are full and active, autostart. active_seats = [x for x in self.seats if x.player] if (self.state.get() == "need_players" and len(active_seats) == self.mode and self.active): self.state.set("playing") self.bc_pre("The game has begun.\n") # Initialize everything by clearing the (non-existent) trick. self.clear_trick() # Pick a hakem at random. self.hakem = random.choice(self.seats) self.bc_pre("Fate has spoken, and the starting hakem is %s!\n" % self.get_sp_str(self.hakem)) # The dealer is always the player before the hakem. self.dealer = self.prev_seat(self.hakem) self.start_deal() def choose(self, player, choose_str): choose_str = choose_str.lower() if choose_str in ( "clubs", "c", ): self.trump_suit = CLUBS elif choose_str in ( "diamonds", "d", ): self.trump_suit = DIAMONDS elif choose_str in ( "hearts", "h", ): self.trump_suit = HEARTS elif choose_str in ( "spades", "s", ): self.trump_suit = SPADES else: self.tell_pre(player, "That's not a valid suit!\n") return # Success. Declare it and finish the deal. self.bc_pre("^Y%s^~ has picked ^R%s^~ as trumps.\n" % (player, self.trump_suit)) self.finish_deal() 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.split() primary = command_bits[0].lower() if state == "setup": if primary in ( "goal", "score", "sc", "g", ): 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 ( "players", "pl", ): if len(command_bits) == 2: self.set_players(player, command_bits[1]) else: self.tell_pre(player, "Invalid players command.\n") handled = True elif primary in ( "short", "sh", ): if len(command_bits) == 2: self.set_short(player, command_bits[1]) else: self.tell_pre(player, "Invalid short 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 == "choosing": if primary in ( "hand", "inventory", "inv", "i", ): if player == self.hakem.player: self.show_hand(player) else: self.tell_pre(player, "You can't look at your cards yet!\n") handled = True elif primary in ( "choose", "trump", "ch", "tr", ): if player == self.hakem.player: if len(command_bits) == 2: self.choose(player, command_bits[1]) else: self.tell_pre(player, "Invalid choose command.\n") else: self.tell_pre(player, "You're not hakem!\n") handled = True elif state == "playing": card_played = False if primary in ( "hand", "inventory", "inv", "i", ): self.show_hand(player) handled = True elif primary in ( "play", "move", "pl", "mv", ): if len(command_bits) == 2: card_played = self.play(player, command_bits[1]) else: self.tell_pre(player, "Invalid play command.\n") handled = True if card_played: # A card hit the table. We need to do stuff. if len(self.trick) == self.mode: # Finish the trick up. self.finish_trick() # Did that end the hand? winner = self.find_hand_winner() if winner: # Yup. Resolve the hand... self.resolve_hand(winner) # And look for a winner. winner = self.find_winner() if winner: # Found a winner. Finish. self.resolve(winner) self.finish() else: # No winner. Redeal. self.start_deal() else: # Trick not over. Rotate. self.turn = self.next_seat(self.turn) self.layout.change_turn(self.turn.data.who) if self.turn.player: self.show_hand(self.turn.player) if not handled: self.tell_pre(player, "Invalid command.\n") def finish_trick(self): # Okay, we have a trick with four cards. Which card won? winner = handle_trick(self.trick, self.trump_suit) # This /should/ just return one seat... winning_seat_list = [x for x in self.seats if x.data.card == winner] if len(winning_seat_list) != 1: self.server.log.log( self.log_prefix + "Something went horribly awry; trick ended without a finish.") self.bc_pre( "Something went horribly wrong; no one won the trick! Tell the admin.\n" ) return winning_seat = winning_seat_list[0] # Print information about the winning card. self.bc_pre("%s wins the trick with ^C%s^~.\n" % (self.get_sp_str(winning_seat), card_to_str(winner, LONG))) # If there are four players, give the trick to the correct partnership. if self.mode == 4: if winning_seat == self.seats[0] or winning_seat == self.seats[2]: self.ns.tricks += 1 else: self.ew.tricks += 1 else: winning_seat.data.tricks += 1 # Clear the trick. self.clear_trick() # Set the next leader to the player who won. self.turn = winning_seat self.layout.change_turn(self.turn.data.who) if self.turn.player: self.show_hand(self.turn.player) def find_hand_winner(self): # In four-player mode, this is actually really simple; winning only # occurs when one side has more than 6 tricks. if self.mode == 4: if self.ns.tricks > 6: return self.ns elif self.ew.tricks > 6: return self.ew else: # In three-player mode, this is considerably less simple. If # one player has more tricks than either other player can possibly # get, they win... tricks_remaining = len(self.west.data.hand) for seat in self.seats: our_tricks = seat.data.tricks prev_tricks = self.prev_seat(seat).data.tricks next_tricks = self.next_seat(seat).data.tricks if ((our_tricks > prev_tricks + tricks_remaining) and (our_tricks > next_tricks + tricks_remaining)): return seat # ...orrr if there are no tricks left and the other two players # tied for the number of tricks, we win as well. 3p Hokm, you # so crazy. if (not tricks_remaining) and prev_tricks == next_tricks: return seat # There's also the case where one player gets the first seven; # this is handled already for the short deck by the first check # above, but has to have a specific check for the long-deck # game. if our_tricks == 7 and not prev_tricks and not next_tricks: return seat # No winner yet. return None def resolve_hand(self, winner): # Assume the hakem won and there was no sweep; we'll adjust later. hakem_won = True swept = False # 4p mode shenanigans first. if self.mode == 4: if winner == self.ns: winning_str = "^RNorth/South^~" if self.hakem != self.seats[0] and self.hakem != self.seats[2]: hakem_won = False loser = self.ew else: winning_str = "^MEast/West^~" if self.hakem != self.seats[1] and self.hakem != self.seats[3]: hakem_won = False loser = self.ns # Did the loser get no tricks? If so, the winner swept! if loser.tricks == 0: swept = True else: # 3P mode. Check whether the hakem really won... if winner != self.hakem: hakem_won = False # ...and whether the winner swept. prev_tricks = self.prev_seat(winner).data.tricks next_tricks = self.next_seat(winner).data.tricks if not prev_tricks and not next_tricks: swept = True winning_str = self.get_sp_str(winner) if swept: action_str = "^Yswept^~" # 2 points if the hakem won, 3 if others did. if hakem_won: addend = 2 else: addend = 3 else: # Standard win. One point. action_str = "^Wwon^~" addend = 1 # Let everyone know. self.bc_pre("%s %s the hand and gains ^C%s^~.\n" % (winning_str, action_str, get_plural_str(addend, "point"))) # Apply the score. if self.mode == 4: winner.score += addend else: winner.data.score += addend # Show everyone's scores. self.bc_pre(self.get_score_str()) # Did the hakem not win? If so, we need to have a new hakem and dealer. if not hakem_won: # In 4p mode, it just rotates... if self.mode == 4: self.dealer = self.hakem self.hakem = self.next_seat(self.hakem) else: # In 3p mode, the winner becomes hakem. self.hakem = winner self.dealer = self.prev_seat(self.hakem) self.bc_pre( "The ^Yhakem^~ has been unseated! The new hakem is %s.\n" % self.get_sp_str(self.hakem)) else: self.bc_pre("%s remains the hakem.\n" % self.get_sp_str(self.hakem)) def find_winner(self): if self.mode == 4: # Easy: has one of the sides reached a winning score? if self.ns.score >= self.goal: return self.ns elif self.ew.score >= self.goal: return self.ew else: # Have any of the players reached a winning score? for seat in self.seats: if seat.data.score >= self.goal: return seat return None def resolve(self, winner): if self.mode == 4: if self.ns == winner: name_one = self.seats[0].player_name name_two = self.seats[2].player_name else: name_one = self.seats[1].player_name name_two = self.seats[3].player_name self.bc_pre("^G%s^~ and ^G%s^~ win!\n" % (name_one, name_two)) else: self.bc_pre("^G%s^~ wins!\n" % winner.player_name)
class Hokm(SeatedGame): """A Hokm game table implementation. Hokm is a Persian trick-taking card game of unknown provenance. This implementation doesn't currently rearrange the seats at the start, but does support both the standard 4p partnership game and the quirky 3p mode. In addition, 3p mode can use both a short deck (13 cards per hand) or a long deck (17). """ def __init__(self, server, table_name): super(Hokm, self).__init__(server, table_name) self.game_display_name = "Hokm" self.game_name = "hokm" self.state = State("need_players") self.prefix = "(^RHokm^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Hokm-specific stuff. self.goal = 7 self.trick = None self.trump_suit = None self.led_suit = None self.turn = None self.dealer = None self.hakem = None self.winner = None # Default to four-player mode. self.mode = 4 self.short = True self.setup_mode() def setup_mode(self): # Sets up all of the structures that depend on the mode of Hokm # we're playing: seats, layouts, and (in 4p mode) partnerships. # Remember that seats in Hokm go the opposite direction of the # American/European standard. if self.mode == 4: self.seats = [ Seat("North"), Seat("West"), Seat("South"), Seat("East"), ] self.seats[0].data.who = NORTH self.seats[1].data.who = WEST self.seats[2].data.who = SOUTH self.seats[3].data.who = EAST self.min_players = 4 self.max_players = 4 self.layout = FourPlayerCardGameLayout() # Set up the partnership structures. self.ns = Struct() self.ew = Struct() self.ns.score = 0 self.ew.score = 0 elif self.mode == 3: self.seats = [ Seat("West"), Seat("South"), Seat("East"), ] self.seats[0].data.who = WEST self.seats[1].data.who = SOUTH self.seats[2].data.who = EAST self.west = self.seats[0] self.south = self.seats[1] self.east = self.seats[2] self.west.data.score = 0 self.south.data.score = 0 self.east.data.score = 0 self.min_players = 3 self.max_players = 3 self.layout = ThreePlayerCardGameLayout() else: self.log_pre("MAJOR ERROR: Hokm initialization with invalid mode %s!" % self.mode) def show_help(self, player): super(Hokm, self).show_help(player) player.tell_cc("\nHOKM SETUP PHASE:\n\n") player.tell_cc(" ^!setup^., ^!config^., ^!conf^. Enter setup phase.\n") player.tell_cc(" ^!goal^. <num>, ^!score^. Set the goal score to <num>.\n") player.tell_cc(" ^!players^. 3|4, ^!pl^. Set the number of players.\n") player.tell_cc(" ^!short^. on|off, ^!sh^. Use a short deck (3p only).\n") player.tell_cc(" ^!ready^., ^!done^., ^!r^., ^!d^. End setup phase.\n") player.tell_cc("\nHOKM PLAY:\n\n") player.tell_cc(" ^!choose^. <suit>, ^!ch^. Declare <suit> as trumps. Hakem only.\n") player.tell_cc(" ^!play^. <card>, ^!pl^. Play <card> from your hand.\n") player.tell_cc(" ^!hand^., ^!inv^., ^!i^. Look at the cards in your hand.\n") def display(self, player): player.tell_cc("%s" % self.layout) def get_color_code(self, seat): if self.mode == 4: if seat == self.seats[0] or seat == self.seats[2]: return "^R" else: return "^M" else: if seat == self.west: return "^M" elif seat == self.south: return "^R" else: return "^B" def get_sp_str(self, seat): return "^G%s^~ (%s%s^~)" % (seat.player_name, self.get_color_code(seat), seat) def get_score_str(self): if self.mode == 4: return " ^RNorth/South^~: %d ^MEast/West^~: %d\n" % (self.ns.score, self.ew.score) else: return " ^M%s^~: %d ^R%s^~: %d ^B%s^~: %d\n" % (self.west.player_name, self.west.data.score, self.south.player_name, self.south.data.score, self.east.player_name, self.east.data.score) def get_metadata(self): to_return = "\n\n" if self.turn: seat_color = self.get_color_code(self.turn) to_return += "%s is the hakem.\n" % (self.get_sp_str(self.hakem)) if self.trump_suit: trump_str = "^C%s^~" % self.trump_suit else: trump_str = "^cwaiting to be chosen^~" to_return += "It is ^Y%s^~'s turn (%s%s^~). Trumps are ^C%s^~.\n" % (self.turn.player_name, seat_color, self.turn, trump_str) if self.mode == 4: to_return += "Tricks: ^RNorth/South^~: %d ^MEast/West^~: %d\n" % (self.ns.tricks, self.ew.tricks) else: to_return += "Tricks: ^M%s^~: %d ^R%s^~: %d ^B%s^~: %d\n" % (self.west.player_name, self.west.data.tricks, self.south.player_name, self.south.data.tricks, self.east.player_name, self.east.data.tricks) to_return += "The goal score for this game is ^C%s^~.\n" % get_plural_str(self.goal, "point") to_return += self.get_score_str() return to_return def show(self, player): self.display(player) player.tell_cc(self.get_metadata()) 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 set_short(self, player, short_bits): if self.mode != 3: self.tell_pre(player, "Cannot set short mode when not in 3-player mode.\n") return False short_bool = booleanize(short_bits) if short_bool: if short_bool > 0: self.short = True display_str = "^Con^~" elif short_bool < 0: self.short = False display_str = "^coff^~" self.bc_pre("^R%s^~ has turned short suits %s.\n" % (player, display_str)) else: self.tell_pre(player, "Not a valid boolean!\n") def set_players(self, player, player_str): if not player_str.isdigit(): self.tell_pre(player, "You didn't even send a number!\n") return False new_mode = int(player_str) if new_mode == self.mode: self.tell_pre(player, "That is the current player count.\n") return False elif new_mode != 3 and new_mode != 4: self.tell_pre(player, "Only 3-player and 4-player Hokm is supported.\n") return False # Got a valid mode. self.mode = new_mode self.bc_pre("^M%s^~ has changed the number of players to ^G%s^~.\n" % (player, new_mode)) self.setup_mode() def clear_trick(self): # Set the current trick to an empty hand... self.trick = Hand() self.led_suit = None # ...and set everyone's played card to None. for seat in self.seats: seat.data.card = None # Clear the layout as well. self.layout.clear() def new_deck(self): # In 4-player mode, it's a standard 52-card pack. if self.mode == 4: self.deck = new_deck() else: # If it's a short deck, 7-A are full; if a long deck, 3-A are. full_ranks = [ACE, KING, QUEEN, JACK, '10', '9', '8', '7', '6'] if self.short: short_rank = '5' else: full_ranks.extend(['5', '4', '3']) short_rank = '2' # Build the deck, full ranks first. self.deck = Hand() for suit in (CLUBS, DIAMONDS, HEARTS, SPADES): for rank in full_ranks: self.deck.add(PlayingCard(rank, suit)) # We only want three of the short rank. No hearts, because. for suit in (CLUBS, DIAMONDS, SPADES): self.deck.add(PlayingCard(short_rank, suit)) def start_deal(self): # Set the trick counts to zero, appropriately for the mode. if self.mode == 4: self.ns.tricks = 0 self.ew.tricks = 0 else: self.west.data.tricks = 0 self.south.data.tricks = 0 self.east.data.tricks = 0 dealer_name = self.dealer.player_name self.bc_pre("^R%s^~ (%s%s^~) gives the cards a good shuffle...\n" % (dealer_name, self.get_color_code(self.dealer), self.dealer)) self.new_deck() self.deck.shuffle() # Deal out five cards each. self.bc_pre("^R%s^~ deals five cards out to each of the players.\n" % dealer_name) for seat in self.seats: seat.data.hand = Hand() for i in range(5): for seat in self.seats: seat.data.hand.add(self.deck.discard()) # Clear the internal metadata about trumps. self.trump_suit = None # Sort the hakem's hand. self.hakem.data.hand = sorted_hand(self.hakem.data.hand) # Show the hakem their hand. if self.hakem.player: self.tell_pre(self.hakem.player, "Please choose a trump suit for this hand.\n") self.show_hand(self.hakem.player) # The hakem both chooses and, eventually, leads. self.turn = self.hakem self.layout.change_turn(self.hakem.data.who) # Shift into "choosing" mode. self.state.set("choosing") def finish_deal(self): self.bc_pre("^R%s^~ finishes dealing the cards out.\n" % self.dealer.player_name) while len(self.deck): for seat in self.seats: seat.data.hand.add(self.deck.discard()) # Sort everyone's hands now that we have a trump suit. for seat in self.seats: seat.data.hand = sorted_hand(seat.data.hand, self.trump_suit) # Show everyone their completed hands. self.show_hands() # We're playing now. self.state.set("playing") 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, self.trump_suit) print_str += "\n" self.tell_pre(player, print_str) def show_hands(self): for seat in self.seats: if seat.player: self.show_hand(seat.player) 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 # 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 # Okay, it's a card in their hand. First, let's do the "follow the # led suit" business. action_str = "^Wplays^~" if self.led_suit: this_suit = potential_card.suit if (this_suit != self.led_suit and hand_has_suit(seat.data.hand, self.led_suit)): # You can't play off-suit if you can match the led suit. self.tell_pre(player, "You can't throw off; you have the led suit.\n") return False else: # No led suit; they're the leader. action_str = "^Yleads^~ with" self.led_suit = potential_card.suit # They either matched the led suit, didn't have any of it, or they # are themselves the leader. Nevertheless, their play is valid. seat.data.card = potential_card self.trick.add(seat.data.hand.discard_specific(potential_card)) trump_str = "" if potential_card.suit == self.trump_suit: trump_str = ", a ^Rtrump^~" self.bc_pre("%s %s ^C%s^~%s.\n" % (self.get_sp_str(seat), action_str, card_to_str(potential_card, LONG), trump_str)) self.layout.place(seat.data.who, potential_card) return potential_card def tick(self): # If all seats are full and active, autostart. active_seats = [x for x in self.seats if x.player] if (self.state.get() == "need_players" and len(active_seats) == self.mode and self.active): self.state.set("playing") self.bc_pre("The game has begun.\n") # Initialize everything by clearing the (non-existent) trick. self.clear_trick() # Pick a hakem at random. self.hakem = random.choice(self.seats) self.bc_pre("Fate has spoken, and the starting hakem is %s!\n" % self.get_sp_str(self.hakem)) # The dealer is always the player before the hakem. self.dealer = self.prev_seat(self.hakem) self.start_deal() def choose(self, player, choose_str): choose_str = choose_str.lower() if choose_str in ("clubs", "c",): self.trump_suit = CLUBS elif choose_str in ("diamonds", "d",): self.trump_suit = DIAMONDS elif choose_str in ("hearts", "h",): self.trump_suit = HEARTS elif choose_str in ("spades", "s",): self.trump_suit = SPADES else: self.tell_pre(player, "That's not a valid suit!\n") return # Success. Declare it and finish the deal. self.bc_pre("^Y%s^~ has picked ^R%s^~ as trumps.\n" % (player, self.trump_suit)) self.finish_deal() 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.split() primary = command_bits[0].lower() if state == "setup": if primary in ("goal", "score", "sc", "g",): 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 ("players", "pl",): if len(command_bits) == 2: self.set_players(player, command_bits[1]) else: self.tell_pre(player, "Invalid players command.\n") handled = True elif primary in ("short", "sh",): if len(command_bits) == 2: self.set_short(player, command_bits[1]) else: self.tell_pre(player, "Invalid short 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 == "choosing": if primary in ("hand", "inventory", "inv", "i",): if player == self.hakem.player: self.show_hand(player) else: self.tell_pre(player, "You can't look at your cards yet!\n") handled = True elif primary in ("choose", "trump", "ch", "tr",): if player == self.hakem.player: if len(command_bits) == 2: self.choose(player, command_bits[1]) else: self.tell_pre(player, "Invalid choose command.\n") else: self.tell_pre(player, "You're not hakem!\n") handled = True elif state == "playing": card_played = False if primary in ("hand", "inventory", "inv", "i",): self.show_hand(player) handled = True elif primary in ("play", "move", "pl", "mv",): if len(command_bits) == 2: card_played = self.play(player, command_bits[1]) else: self.tell_pre(player, "Invalid play command.\n") handled = True if card_played: # A card hit the table. We need to do stuff. if len(self.trick) == self.mode: # Finish the trick up. self.finish_trick() # Did that end the hand? winner = self.find_hand_winner() if winner: # Yup. Resolve the hand... self.resolve_hand(winner) # And look for a winner. winner = self.find_winner() if winner: # Found a winner. Finish. self.resolve(winner) self.finish() else: # No winner. Redeal. self.start_deal() else: # Trick not over. Rotate. self.turn = self.next_seat(self.turn) self.layout.change_turn(self.turn.data.who) if self.turn.player: self.show_hand(self.turn.player) if not handled: self.tell_pre(player, "Invalid command.\n") def finish_trick(self): # Okay, we have a trick with four cards. Which card won? winner = handle_trick(self.trick, self.trump_suit) # This /should/ just return one seat... winning_seat_list = [x for x in self.seats if x.data.card == winner] if len(winning_seat_list) != 1: self.server.log.log(self.log_prefix + "Something went horribly awry; trick ended without a finish.") self.bc_pre("Something went horribly wrong; no one won the trick! Tell the admin.\n") return winning_seat = winning_seat_list[0] # Print information about the winning card. self.bc_pre("%s wins the trick with ^C%s^~.\n" % (self.get_sp_str(winning_seat), card_to_str(winner, LONG))) # If there are four players, give the trick to the correct partnership. if self.mode == 4: if winning_seat == self.seats[0] or winning_seat == self.seats[2]: self.ns.tricks += 1 else: self.ew.tricks += 1 else: winning_seat.data.tricks += 1 # Clear the trick. self.clear_trick() # Set the next leader to the player who won. self.turn = winning_seat self.layout.change_turn(self.turn.data.who) if self.turn.player: self.show_hand(self.turn.player) def find_hand_winner(self): # In four-player mode, this is actually really simple; winning only # occurs when one side has more than 6 tricks. if self.mode == 4: if self.ns.tricks > 6: return self.ns elif self.ew.tricks > 6: return self.ew else: # In three-player mode, this is considerably less simple. If # one player has more tricks than either other player can possibly # get, they win... tricks_remaining = len(self.west.data.hand) for seat in self.seats: our_tricks = seat.data.tricks prev_tricks = self.prev_seat(seat).data.tricks next_tricks = self.next_seat(seat).data.tricks if ((our_tricks > prev_tricks + tricks_remaining) and (our_tricks > next_tricks + tricks_remaining)): return seat # ...orrr if there are no tricks left and the other two players # tied for the number of tricks, we win as well. 3p Hokm, you # so crazy. if (not tricks_remaining) and prev_tricks == next_tricks: return seat # There's also the case where one player gets the first seven; # this is handled already for the short deck by the first check # above, but has to have a specific check for the long-deck # game. if our_tricks == 7 and not prev_tricks and not next_tricks: return seat # No winner yet. return None def resolve_hand(self, winner): # Assume the hakem won and there was no sweep; we'll adjust later. hakem_won = True swept = False # 4p mode shenanigans first. if self.mode == 4: if winner == self.ns: winning_str = "^RNorth/South^~" if self.hakem != self.seats[0] and self.hakem != self.seats[2]: hakem_won = False loser = self.ew else: winning_str = "^MEast/West^~" if self.hakem != self.seats[1] and self.hakem != self.seats[3]: hakem_won = False loser = self.ns # Did the loser get no tricks? If so, the winner swept! if loser.tricks == 0: swept = True else: # 3P mode. Check whether the hakem really won... if winner != self.hakem: hakem_won = False # ...and whether the winner swept. prev_tricks = self.prev_seat(winner).data.tricks next_tricks = self.next_seat(winner).data.tricks if not prev_tricks and not next_tricks: swept = True winning_str = self.get_sp_str(winner) if swept: action_str = "^Yswept^~" # 2 points if the hakem won, 3 if others did. if hakem_won: addend = 2 else: addend = 3 else: # Standard win. One point. action_str = "^Wwon^~" addend = 1 # Let everyone know. self.bc_pre("%s %s the hand and gains ^C%s^~.\n" % (winning_str, action_str, get_plural_str(addend, "point"))) # Apply the score. if self.mode == 4: winner.score += addend else: winner.data.score += addend # Show everyone's scores. self.bc_pre(self.get_score_str()) # Did the hakem not win? If so, we need to have a new hakem and dealer. if not hakem_won: # In 4p mode, it just rotates... if self.mode == 4: self.dealer = self.hakem self.hakem = self.next_seat(self.hakem) else: # In 3p mode, the winner becomes hakem. self.hakem = winner self.dealer = self.prev_seat(self.hakem) self.bc_pre("The ^Yhakem^~ has been unseated! The new hakem is %s.\n" % self.get_sp_str(self.hakem)) else: self.bc_pre("%s remains the hakem.\n" % self.get_sp_str(self.hakem)) def find_winner(self): if self.mode == 4: # Easy: has one of the sides reached a winning score? if self.ns.score >= self.goal: return self.ns elif self.ew.score >= self.goal: return self.ew else: # Have any of the players reached a winning score? for seat in self.seats: if seat.data.score >= self.goal: return seat return None def resolve(self, winner): if self.mode == 4: if self.ns == winner: name_one = self.seats[0].player_name name_two = self.seats[2].player_name else: name_one = self.seats[1].player_name name_two = self.seats[3].player_name self.bc_pre("^G%s^~ and ^G%s^~ win!\n" % (name_one, name_two)) else: self.bc_pre("^G%s^~ wins!\n" % winner.player_name)
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")
class Talpa(SeatedGame): """A Talpa game table implementation. Invented in 2010 by Arty Sandler. """ def __init__(self, server, table_name): super(Talpa, self).__init__(server, table_name) self.game_display_name = "Talpa" self.game_name = "talpa" self.seats = [Seat("Red"), Seat("Blue")] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.prefix = "(^RTalpa^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Talpa-specific stuff. self.size = 8 self.turn = None self.red = self.seats[0] self.red.data.seat_str = "^RRed/Vertical^~" self.blue = self.seats[1] self.blue.data.seat_str = "^BBlue/Horizontal^~" self.resigner = None self.layout = None # Like in most connection games, there is no difference between pieces # of a given color, so we save time and create our singleton pieces # here. self.rp = Piece("^R", "x", "X") self.rp.data.owner = self.red self.bp = Piece("^B", "o", "O") self.bp.data.owner = self.blue # Initialize the starting layout. self.init_layout() def init_layout(self): # Create the layout and fill it with pieces. self.layout = SquareGridLayout(highlight_color="^I") self.layout.resize(self.size) for i in range(self.size): for j in range(self.size): if (i + j) % 2: self.layout.place(self.rp, i, j, update=False) else: self.layout.place(self.bp, i, j, update=False) self.layout.update() def get_sp_str(self, seat): return "^C%s^~ (%s)" % (seat.player_name, seat.data.seat_str) def get_turn_str(self): if not self.turn: return "The game has not yet started.\n" return "It is ^C%s^~'s turn (%s).\n" % (self.turn.player_name, self.turn.data.seat_str) def show(self, player): player.tell_cc(self.layout) player.tell_cc(self.get_turn_str()) def send_board(self): for player in self.channel.listeners: self.show(player) def set_size(self, player, size_str): if not size_str.isdigit(): self.tell_pre(player, "You didn't even send a number!\n") return False size = int(size_str) if size < MIN_SIZE or size > MAX_SIZE: self.tell_pre(player, "Size must be between %d and %d inclusive.\n" % (MIN_SIZE, MAX_SIZE)) return False # Size must be even. if size % 2: self.tell_pre(player, "Size must be even.\n") return False # Valid! self.size = size self.bc_pre("^R%s^~ has set the board size to ^C%d^~.\n" % (player, size)) self.init_layout() def move(self, player, src_bits, dst_bits): seat = self.get_seat_of_player(player) if not seat: self.tell_pre(player, "You can't move; you're not playing!\n") return False if seat != self.turn: self.tell_pre(player, "You must wait for your turn to move.\n") return False src_c, src_r = src_bits dst_c, dst_r = dst_bits # Are they valid? if not self.layout.is_valid(src_r, src_c) or not self.layout.is_valid(dst_r, dst_c): self.tell_pre(player, "Your move is out of bounds.\n") return False # Is it an orthogonal move? c_delta = abs(src_c - dst_c) r_delta = abs(src_r - dst_r) if c_delta > 1 or r_delta > 1 or (c_delta and r_delta): self.tell_pre(player, "You can only make a single-space orthogonal move.\n") return False # Is there a piece for this player in the source location? src_loc = self.layout.grid[src_r][src_c] if not src_loc or src_loc.data.owner != seat: self.tell_pre(player, "You must have a piece in the source location.\n") return False # Is there a piece for the other player in the destination location? dst_loc = self.layout.grid[dst_r][dst_c] if not dst_loc or dst_loc.data.owner == seat: self.tell_pre(player, "Your opponent must have a piece in the destination location.\n") return False # Phew. Success. Make the capture. src_str = "%s%s" % (COLS[src_c], src_r + 1) dst_str = "%s%s" % (COLS[dst_c], dst_r + 1) self.bc_pre("%s moves a piece from ^C%s^~ to ^G%s^~.\n" % (self.get_sp_str(seat), src_str, dst_str)) self.layout.move(src_r, src_c, dst_r, dst_c, True) return True def has_capture(self, seat): # We loop through the board, checking each piece to see if it's for # this player. If so, we check its four adjacencies to see if one # of them is for the other player. If so, there's still a valid # capture this player can make. for r in range(self.size): for c in range(self.size): loc = self.layout.grid[r][c] if loc and loc.data.owner == seat: for r_delta, c_delta in CONNECTION_DELTAS: new_r = r + r_delta new_c = c + c_delta if self.layout.is_valid(new_r, new_c): dst = self.layout.grid[new_r][new_c] if dst and dst.data.owner != seat: return True # We never found a valid capture. return False def remove(self, player, remove_bits): seat = self.get_seat_of_player(player) if not seat: self.tell_pre(player, "You can't remove; you're not playing!\n") return False if seat != self.turn: self.tell_pre(player, "You must wait for your turn to remove.\n") return False c, r = remove_bits # Is it within bounds? if not self.layout.is_valid(r, c): self.tell_pre(player, "Your remove is out of bounds.\n") return False # Does this player have a piece there? loc = self.layout.grid[r][c] if not loc or loc.data.owner != seat: self.tell_pre(player, "You must have a piece there to remove.\n") return False # Do they have a valid capture instead? if self.has_capture(seat): self.tell_pre(player, "You have a capture left.\n") return False # All right, remove the piece. loc_str = "%s%s" % (COLS[c], r + 1) self.bc_pre("%s removes a piece from ^R%s^~.\n" % (self.get_sp_str(seat), loc_str)) self.layout.remove(r, c, True) 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 seat != self.turn: 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 tick(self): # If both seats are occupied and the game is active, start. if self.state.get() == "need_players" and self.red.player and self.blue.player and self.active: self.bc_pre( "%s: ^C%s^~; %s: ^C%s^~\n" % (self.red.data.seat_str, self.red.player_name, self.blue.data.seat_str, self.blue.player_name) ) self.state.set("playing") self.turn = self.red self.send_board() 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 ("size", "sz"): if len(command_bits) == 2: self.set_size(player, command_bits[1]) else: self.tell_pre(player, "Invalid size 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.bc_pre("^R%s^~ has switched the game to setup mode.\n" % player) self.state.set("setup") handled = True elif state == "playing": made_move = False if primary in ("move", "play", "mv", "pl"): move_bits = demangle_move(command_bits[1:]) if move_bits and len(move_bits) == 2: made_move = self.move(player, move_bits[0], move_bits[1]) else: self.tell_pre(player, "Invalid move command.\n") handled = True elif primary in ("remove", "re"): move_bits = demangle_move(command_bits[1:]) if move_bits and len(move_bits) == 1: made_move = self.remove(player, move_bits[0]) else: self.tell_pre(player, "Invalid remove command.\n") handled = True elif primary in ("resign",): made_move = self.resign(player) handled = True if made_move: # Did someone win? winner = self.find_winner() if winner: # Yup! self.resolve(winner) self.finish() else: # No. Change turns and send the board to listeners. self.turn = self.next_seat(self.turn) self.send_board() if not handled: self.tell_pre(player, "Invalid command.\n") def find_winner(self): # Did someone resign? if self.resigner == self.red: return self.blue elif self.resigner == self.blue: return self.red # Like most connection games, we will do a recursive check. Unlike # most connection games, we're looking for a lack of pieces, not # their existence. In addition, if both players won at the same # time, the mover loses. We need two distinct adjacency maps since # we're looking at blank spaces, not pieces of a given color, and # those blank spaces can be used by either side. self.blue.data.adjacency_map = [] self.red.data.adjacency_map = [] for i in range(self.size): self.blue.data.adjacency_map.append([None] * self.size) self.red.data.adjacency_map.append([None] * self.size) self.red.data.won = False self.blue.data.won = False for i in range(self.size): if not self.red.data.won and not self.layout.grid[0][i]: self.recurse(self.red, 0, i) if not self.blue.data.won and not self.layout.grid[i][0]: self.recurse(self.blue, i, 0) # Handle the double-win state (mover loses) first. if self.red.data.won and self.blue.data.won: if self.turn == self.red: return self.blue else: return self.red # Now, normal winning states. elif self.red.data.won: return self.red elif self.blue.data.won: return self.blue # No winner. return None def recurse(self, seat, row, col): # Bail if this seat's already won. if seat.data.won: return # Bail if we're off the board. if not self.layout.is_valid(row, col): return # Bail if we've been here. if seat.data.adjacency_map[row][col]: return # Bail if there's a piece here. if self.layout.grid[row][col]: return # All right. Empty and we haven't been here. Mark. seat.data.adjacency_map[row][col] = True # Did we hit the winning side for this player? if seat == self.blue and col == self.size - 1: seat.data.won = True return elif seat == self.red and row == self.size - 1: seat.data.won = True return # Not a win yet. Recurse over adjacencies. for r_delta, c_delta in CONNECTION_DELTAS: self.recurse(seat, row + r_delta, col + c_delta) def resolve(self, winner): self.send_board() self.bc_pre("%s wins!\n" % self.get_sp_str(winner)) def show_help(self, player): super(Talpa, self).show_help(player) player.tell_cc("\nTALPA SETUP PHASE:\n\n") player.tell_cc(" ^!setup^., ^!config^., ^!conf^. Enter setup phase.\n") player.tell_cc(" ^!size^. <size>, ^!sz^. Set board to <size>.\n") player.tell_cc(" ^!ready^., ^!done^., ^!r^., ^!d^. End setup phase.\n") player.tell_cc("\nTALPA PLAY:\n\n") player.tell_cc(" ^!move^. <ln> <ln2>, ^!mv^. Move from <ln> to <ln2> (letter number).\n") player.tell_cc(" ^!remove^. <ln> ^!re^. Remove piece at <ln> (letter number).\n") player.tell_cc(" ^!resign^. Resign.\n")
class SquareOust(SeatedGame): """A Square Oust game table implementation. Invented in 2007 by Mark Steere. """ def __init__(self, server, table_name): super(SquareOust, self).__init__(server, table_name) self.game_display_name = "Square Oust" self.game_name = "square_oust" self.seats = [ Seat("Black"), Seat("White"), ] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.prefix = "(^RSquare Oust^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Square Oust-specific stuff. self.height = 11 self.width = 11 self.turn = None self.black = self.seats[0] self.black.data.seat_str = "^KBlack^~" self.black.data.groups = [] self.black.data.made_move = False self.white = self.seats[1] self.white.data.seat_str = "^WWhite^~" self.white.data.groups = [] self.white.data.made_move = False self.resigner = None self.layout = None self.move_was_capture = False # Initialize the starting layout. self.init_layout() def init_layout(self): # Create the layout. Empty, so easy. self.layout = SquareGridLayout(highlight_color="^I") self.layout.resize(self.width, self.height) def get_sp_str(self, seat): return "^C%s^~ (%s)" % (seat.player_name, seat.data.seat_str) def get_turn_str(self): if not self.turn: return "The game has not yet started.\n" return "It is ^C%s^~'s turn (%s).\n" % (self.turn.player_name, self.turn.data.seat_str) def show(self, player): player.tell_cc(self.layout) player.tell_cc(self.get_turn_str()) def send_board(self): for player in self.channel.listeners: self.show(player) def set_size(self, player, size_bits): # Is there an 'x' in the middle of a single argument? if len(size_bits) == 1: size_bits[0] = size_bits[0].lower() if "x" in size_bits[0]: size_bits = size_bits[0].split("x") width = size_bits[0] # If there's a single element, height == width. if len(size_bits) == 1: height = width elif len(size_bits) == 2: width = size_bits[0] height = size_bits[1] else: self.tell_pre(player, "Invalid size command.\n") return if not width.isdigit() or not height.isdigit(): self.tell_pre(player, "You didn't even send numbers!\n") return w = int(width) h = int(height) if w < MIN_SIZE or w > MAX_SIZE or h < MIN_SIZE or h > MAX_SIZE: self.tell_pre( player, "Width and height must be between %d and %d inclusive.\n" % (MIN_SIZE, MAX_SIZE)) return # Valid! self.width = w self.height = h self.bc_pre("^R%s^~ has set the board size to ^C%d^Gx^C%d^~.\n" % (player, w, h)) self.init_layout() def get_new_piece(self, seat): if seat == self.black: p = Piece("^K", "x", "X") else: p = Piece("^W", "o", "O") p.data.owner = seat p.data.adjacencies = [] p.data.size = 1 p.data.num = id(p) return p def replace(self, old, new): # First step: Replace the actual pieces on the board. for r in range(self.height): for c in range(self.width): if self.layout.grid[r][c] == old: self.layout.place(new, r, c, update=False) self.layout.update() # Second step: Get rid of it from the group list of its owner. owner = new.data.owner owner.data.groups.remove(old) # Third step: Replace instances of it in the other player's group # adjacencies. for group in self.next_seat(owner).data.groups: if old in group.data.adjacencies: group.data.adjacencies.remove(old) if new not in group.data.adjacencies: group.data.adjacencies.append(new) def remove(self, dead_group): # Like above, except removing this time. for r in range(self.height): for c in range(self.width): if self.layout.grid[r][c] == dead_group: self.layout.remove(r, c, update=False) self.layout.update() owner = dead_group.data.owner owner.data.groups.remove(dead_group) for group in self.next_seat(owner).data.groups: if dead_group in group.data.adjacencies: group.data.adjacencies.remove(dead_group) def update_board(self, row, col): # We just put a fresh piece at this location; it will have to be # incorporated into everything else that's on the board. this_piece = self.layout.grid[row][col] # Look at all of the adjacencies and collapse the same-color groups # into one. Collate the unique enemy groups as well, as we may be # capturing them. other_adjacencies = [] potential_capture = False for r_delta, c_delta in CONNECTION_DELTAS: new_r = row + r_delta new_c = col + c_delta if self.layout.is_valid(new_r, new_c): loc = self.layout.grid[new_r][new_c] if loc and loc.data.owner == this_piece.data.owner: potential_capture = True if loc != this_piece: # New same-color group to collapse. other_adjacencies.extend([ x for x in loc.data.adjacencies if x not in other_adjacencies ]) new_size = this_piece.data.size + loc.data.size if this_piece.data.num < loc.data.num: self.replace(loc, this_piece) else: self.replace(this_piece, loc) this_piece = loc # Whichever "won," set the new size. this_piece.data.size = new_size elif loc: # A group of the other player. Add to other adjacencies if # it's not already there. if loc not in other_adjacencies: other_adjacencies.append(loc) # After having collapsed all of the same-colored groups, we look to see # if this is a potential capture. If not, we can't affect the opponent's # groups, other than to become adjacent to them. If so, all groups in the # list of other adjacencies must be removed from the board. if potential_capture: for group in other_adjacencies: self.remove(group) # By definition, a capturing group has no enemy adjacencies. this_piece.data.adjacencies = [] # Return the number of groups we captured. return len(other_adjacencies) else: # Set the adjacency list. this_piece.data.adjacencies = other_adjacencies # Add ourselves to those pieces' adjacency lists. for group in other_adjacencies: if this_piece not in group.data.adjacencies: group.data.adjacencies.append(this_piece) # No captures. return 0 def move(self, player, move_bits): seat = self.get_seat_of_player(player) if not seat: self.tell_pre(player, "You can't move; you're not playing!\n") return False if seat != self.turn: self.tell_pre(player, "You must wait for your turn to move.\n") return False col, row = move_bits # Is the move on the board? if not self.layout.is_valid(row, col): self.tell_pre(player, "Your move is out of bounds.\n") return False # Is this move a valid play? if not self.is_valid_play(seat, row, col): self.tell_pre(player, "That move is not valid.\n") return False # Valid. Put a piece there. move_str = "%s%s" % (COLS[col], row + 1) piece = self.get_new_piece(seat) seat.data.groups.append(piece) self.layout.place(piece, row, col, True) # Update the board, making any captures. capture_str = "" self.move_was_capture = False capture_count = self.update_board(row, col) if capture_count: capture_str = ", ^Ycapturing %s^~" % (get_plural_str( capture_count, "group")) self.move_was_capture = True self.bc_pre("%s places a piece at ^C%s^~%s.\n" % (self.get_sp_str(seat), move_str, capture_str)) seat.data.made_move = True 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 seat != self.turn: 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 tick(self): # If both seats are occupied and the game is active, start. if (self.state.get() == "need_players" and self.black.player and self.white.player and self.active): self.bc_pre("%s: ^C%s^~; %s: ^C%s^~\n" % (self.black.data.seat_str, self.black.player_name, self.white.data.seat_str, self.white.player_name)) self.state.set("playing") self.turn = self.black self.send_board() 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 ("size", "sz"): self.set_size(player, command_bits[1:]) 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.bc_pre( "^R%s^~ has switched the game to setup mode.\n" % player) self.state.set("setup") handled = True elif state == "playing": made_move = False if primary in ( "move", "play", "mv", "pl", ): move_bits = demangle_move(command_bits[1:]) if move_bits and len(move_bits) == 1: made_move = self.move(player, move_bits[0]) else: self.tell_pre(player, "Invalid move command.\n") handled = True elif primary in ("resign", ): made_move = self.resign(player) handled = True if made_move: # Did someone win? winner = self.find_winner() if winner: # Yup! self.resolve(winner) self.finish() else: # No. If the move was not a capturing move, see if the # next player has a move; if so, change turns. If not, # print a message and stay here. other = self.next_seat(self.turn) if not self.move_was_capture: if not self.has_move(other): self.bc_pre( "%s has no valid move; ^Rskipping their turn^~.\n" % self.get_sp_str(other)) else: self.turn = other elif not self.has_move(self.turn): self.bc_pre("%s has no further valid moves.\n" % self.get_sp_str(self.turn)) self.turn = other else: self.bc_pre("%s continues their turn.\n" % self.get_sp_str(self.turn)) # No matter what, send the board again. self.send_board() if not handled: self.tell_pre(player, "Invalid command.\n") def is_valid_play(self, seat, row, col): # Obviously we can't place a piece if there's already one here or we're # out of bounds. if not self.layout.is_valid(row, col) or self.layout.grid[row][col]: return False # Okay; can we place a piece here? Check the adjacent spaces; if there # are any pieces owned by this seat, the sum total of their sizes must # be equal to the largest enemy group adjacent either to them or this # new piece. If there are no pieces owned by this seat, it's valid. same_list = [] same_total = 0 largest_other = 0 for r_delta, c_delta in CONNECTION_DELTAS: new_r = row + r_delta new_c = col + c_delta if self.layout.is_valid(new_r, new_c): loc = self.layout.grid[new_r][new_c] if loc and loc.data.owner == seat: if loc not in same_list: same_list.append(loc) same_total += loc.data.size for other in loc.data.adjacencies: if other.data.size > largest_other: largest_other = other.data.size elif loc: if loc.data.size > largest_other: largest_other = loc.data.size # If we didn't find an adjacent same-colored piece, it is immediately # valid. if not same_total: return True # If we found same-colored pieces but no other groups, this is not a # valid play. if not largest_other: return False # Otherwise, check the sum from the same_list and see if that equals # or is greater than the largest other group. if same_total >= largest_other: return True # Not a valid play. return False def has_move(self, seat): for r in range(self.height): for c in range(self.width): if self.is_valid_play(seat, r, c): return True # We checked every location and found no legitimate location. No move. return False def find_winner(self): # Did someone resign? if self.resigner == self.white: return self.black elif self.resigner == self.black: return self.white # If one player has no pieces left, the other player won. if not len(self.white.data.groups) and self.white.data.made_move: return self.black elif not len(self.black.data.groups) and self.black.data.made_move: return self.white # No winner. return None def resolve(self, winner): self.send_board() self.bc_pre("%s wins!\n" % self.get_sp_str(winner)) def show_help(self, player): super(SquareOust, self).show_help(player) player.tell_cc("\nSQUARE OUST SETUP PHASE:\n\n") player.tell_cc( " ^!setup^., ^!config^., ^!conf^. Enter setup phase.\n" ) player.tell_cc( " ^!size^. <size>, ^!sz^. Set board to <size>.\n") player.tell_cc( " ^!ready^., ^!done^., ^!r^., ^!d^. End setup phase.\n" ) player.tell_cc("\nSQUARE OUST PLAY:\n\n") player.tell_cc( " ^!move^. <ln>, ^!play^., ^!mv^., ^!pl^. Make move <ln> (letter number).\n" ) player.tell_cc(" ^!resign^. Resign.\n")
class Crossway(SeatedGame): """A Crossway game table implementation. Invented in 2007 by Mark Steere. """ def __init__(self, server, table_name): super(Crossway, self).__init__(server, table_name) self.game_display_name = "Crossway" self.game_name = "crossway" self.seats = [ Seat("Black"), Seat("White") ] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.prefix = "(^RCrossway^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Crossway-specific stuff. self.board = None self.printable_board = None self.size = 19 self.turn = None self.turn_number = 0 self.seats[0].data.side = BLACK self.seats[1].data.side = WHITE self.last_r = None self.last_c = None self.resigner = None self.adjacency_map = None self.found_winner = False self.init_board() def init_board(self): self.board = [] # Generate a new empty board. for r in range(self.size): self.board.append([None] * self.size) def update_printable_board(self): self.printable_board = [] col_str = " " + "".join([" " + COLS[i] for i in range(self.size)]) self.printable_board.append(col_str + "\n") self.printable_board.append(" ^m.=" + "".join(["=="] * self.size) + ".^~\n") for r in range(self.size): this_str = "%2d ^m|^~ " % (r + 1) for c in range(self.size): if r == self.last_r and c == self.last_c: this_str += "^5" loc = self.board[r][c] if loc == WHITE: this_str += "^Wo^~ " elif loc == BLACK: this_str += "^Kx^~ " else: this_str += "^M.^~ " this_str += "^m|^~ %d" % (r + 1) self.printable_board.append(this_str + "\n") self.printable_board.append(" ^m`=" + "".join(["=="] * self.size) + "'^~\n") self.printable_board.append(col_str + "\n") def show(self, player): if not self.printable_board: self.update_printable_board() for line in self.printable_board: player.tell_cc(line) player.tell_cc(self.get_turn_str() + "\n") def send_board(self): for player in self.channel.listeners: self.show(player) def get_turn_str(self): if not self.turn: return ("The game has not yet started.\n") if self.turn == BLACK: player = self.seats[0].player_name color_msg = "^KBlack/Vertical^~" else: player = self.seats[1].player_name color_msg = "^WWhite/Horizontal^~" return ("It is ^Y%s^~'s turn (%s)." % (player, color_msg)) def is_valid(self, row, col): if row < 0 or row >= self.size or col < 0 or col >= self.size: return False return True def is_checkerboard(self, color, row, col): # Bail immediately if we're given bad input. if not self.is_valid(row, col): return False if self.board[row][col]: return False # Okay. Let's check all four checkerboard deltas. found = False for r_delta, c_delta in CHECKERBOARD_DELTAS: # Only bother if this is on the board or we already found it. if not found and self.is_valid(row + r_delta, col + c_delta): # If the delta space is this color, and the two adjacent spaces # in that direction are the other color, it's a checkerboard. if self.board[row + r_delta][col + c_delta] == color: corner_one = self.board[row + r_delta][col] corner_two = self.board[row][col + c_delta] if (corner_one and corner_one == corner_two and corner_one != color): # Not empty, is another color. Checkerboard. found = True return found def move(self, player, play): seat = self.get_seat_of_player(player) if not seat: player.tell_cc(self.prefix + "You can't move; you're not playing!\n") return False if self.turn != seat.data.side: player.tell_cc(self.prefix + "You must wait for your turn to move.\n") return False col, row = play # Make sure they're all in range. if not self.is_valid(row, col): player.tell_cc(self.prefix + "Your move is out of bounds.\n") return False # Is the space empty? if self.board[row][col]: player.tell_cc(self.prefix + "That space is already occupied.\n") return False # Does the move violate the no-checkerboard rule? if self.is_checkerboard(self.turn, row, col): player.tell_cc(self.prefix + "That move creates a checkerboard.\n") return False # This is a valid move. Apply, announce. self.board[row][col] = self.turn play_str = "%s%s" % (COLS[col], row + 1) self.channel.broadcast_cc(self.prefix + "^Y%s^~ places a piece at ^C%s^~.\n" % (seat.player, play_str)) self.last_r = row self.last_c = col self.turn_number += 1 return True 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.channel.broadcast_cc(self.prefix + "^KBlack/Vertical^~: ^R%s^~; ^WWhite/Horizontal^~: ^Y%s^~\n" % (self.seats[0].player, self.seats[1].player)) self.turn = BLACK self.turn_number = 1 self.send_board() def set_size(self, player, size_bits): if not size_bits.isdigit(): player.tell_cc(self.prefix + "Invalid size command.\n") return size = int(size_bits) if size < MIN_SIZE or size > MAX_SIZE: player.tell_cc(self.prefix + "Size must be between %d and %d inclusive.\n" % (MIN_SIZE, MAX_SIZE)) return # Valid! self.size = size self.channel.broadcast_cc(self.prefix + "^R%s^~ has set the board size to ^C%d^~.\n" % (player, size)) self.init_board() self.update_printable_board() def resign(self, player): seat = self.get_seat_of_player(player) if not seat: player.tell_cc(self.prefix + "You can't resign; you're not playing!\n") return False if self.turn != seat.data.side: player.tell_cc(self.prefix + "You must wait for your turn to resign.\n") return False self.resigner = seat.data.side self.channel.broadcast_cc(self.prefix + "^R%s^~ is resigning from the game.\n" % player) return True def swap(self, player): # Like Hex, a swap in Crossway requires a translation to make it the # equivalent move for the other player. self.board[self.last_r][self.last_c] = None self.board[self.last_c][self.last_r] = WHITE self.last_c, self.last_r = self.last_r, self.last_c self.channel.broadcast_cc("^Y%s^~ has swapped ^KBlack^~'s first move.\n" % (player)) self.turn_number += 1 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 ("size", "sz",): if len(command_bits) == 2: self.set_size(player, command_bits[1]) else: player.tell_cc(self.prefix + "Invalid size command.\n") handled = True if primary in ("done", "ready", "d", "r",): self.channel.broadcast_cc(self.prefix + "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.channel.broadcast_cc(self.prefix + "^R%s^~ has switched the game to setup mode.\n" % player) handled = True elif state == "playing": made_move = False if primary in ("move", "play", "mv", "pl",): invalid = False move_bits = demangle_move(command_bits[1:]) if move_bits and len(move_bits) == 1: made_move = self.move(player, move_bits[0]) else: invalid = True if invalid: player.tell_cc(self.prefix + "Invalid move command.\n") handled = True elif primary in ("swap",): if self.seats[1].player == player and self.turn_number == 2: self.swap(player) made_move = True else: player.tell_cc(self.prefix + "Invalid swap command.\n") handled = True elif primary in ("resign",): if self.resign(player): made_move = True handled = True if made_move: # Okay, something happened on the board. Update. self.update_printable_board() # Did someone win? winner = self.find_winner() if winner: self.resolve(winner) self.finish() else: # Nope. Switch turns... if self.turn == BLACK: self.turn = WHITE else: self.turn = BLACK # ...show everyone the board, and keep on. self.send_board() if not handled: player.tell_cc(self.prefix + "Invalid command.\n") def find_winner(self): # If someone resigned, this is the easiest thing ever. if self.resigner == WHITE: return self.seats[0].player_name elif self.resigner == BLACK: return self.seats[1].player_name # This is like most connection games; we check recursively from the # top and left edges to see whether a player has won. self.found_winner = False self.adjacency_map = [] for i in range(self.size): self.adjacency_map.append([None] * self.size) for i in range(self.size): if self.board[i][0] == WHITE: self.recurse_adjacency(WHITE, i, 0) if self.board[0][i] == BLACK: self.recurse_adjacency(BLACK, 0, i) if self.found_winner == BLACK: return self.seats[0].player_name elif self.found_winner == WHITE: return self.seats[1].player_name # No winner yet. return None def recurse_adjacency(self, color, row, col): # Bail if we found a winner already. if self.found_winner: return # Bail if we're off the board. if not self.is_valid(row, col): return # Bail if we've been here. if self.adjacency_map[row][col]: return # Bail if this is the wrong color. if self.board[row][col] != color: return # Okay. Occupied and it's this player's. Mark. self.adjacency_map[row][col] = True # Have we hit the winning side for this player? if ((color == WHITE and col == self.size - 1) or (color == BLACK and row == self.size - 1)): # Success! self.found_winner = color return # Not a win yet. Recurse over adjacencies. for r_delta, c_delta in CONNECTION_DELTAS: self.recurse_adjacency(color, row + r_delta, col + c_delta) def resolve(self, winner): self.send_board() self.channel.broadcast_cc(self.prefix + "^C%s^~ wins!\n" % winner) def show_help(self, player): super(Crossway, self).show_help(player) player.tell_cc("\nCROSSWAY SETUP PHASE:\n\n") player.tell_cc(" ^!setup^., ^!config^., ^!conf^. Enter setup phase.\n") player.tell_cc(" ^!size^. <size>, ^!sz^. Set board to <size>.\n") player.tell_cc(" ^!ready^., ^!done^., ^!r^., ^!d^. End setup phase.\n") player.tell_cc("\nCROSSWAY PLAY:\n\n") player.tell_cc(" ^!move^. <ln>, ^!play^., ^!mv^., ^!pl^. Make move <ln> (letter number).\n") player.tell_cc(" ^!swap^. Swap the first move (only White, only their first).\n") player.tell_cc(" ^!resign^. Resign.\n")
class Gonnect(SeatedGame): """A Gonnect table implementation. Gonnect was invented by Joao Pedro Neto in 2000. """ def __init__(self, server, table_name): super(Gonnect, self).__init__(server, table_name) self.game_display_name = "Gonnect" self.game_name = "gonnect" self.seats = [ Seat("Black"), Seat("White") ] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.prefix = "(^RGonnect^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Gonnect-specific stuff. self.turn = None self.seats[0].data.side = BLACK self.seats[0].data.dir_str = "/Vertical" self.seats[1].data.side = WHITE self.seats[1].data.dir_str = "/Horizontal" self.directional = False self.resigner = None self.turn_number = 0 self.goban = giles.games.goban.Goban() self.adjacency_map = None self.found_winner = False # A traditional Gonnect board is 13x13. self.goban.resize(13, 13) def show(self, player): if not self.goban.printable_board: self.goban.update_printable_board() for line in self.goban.printable_board: player.tell_cc(line) player.tell_cc(self.get_supplemental_str()) def send_board(self): for player in self.channel.listeners: self.show(player) def get_stone_str(self, count): if count == 1: return "1 stone" return "%d stones" % count def get_supplemental_str(self): if not self.turn: return ("The game has not yet started.\n") dir_str = "" if self.turn == BLACK: player = self.seats[0].player_name if self.directional: dir_str = self.seats[0].data.dir_str color_msg = "^KBlack" + dir_str + "^~" else: player = self.seats[1].player_name if self.directional: dir_str = self.seats[1].data.dir_str color_msg = "^WWhite" + dir_str + "^~" to_return = "It is ^Y%s^~'s turn (%s).\n" % (player, color_msg) return(to_return) 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") if self.directional: black_dir_str = self.seats[0].data.dir_str white_dir_str = self.seats[1].data.dir_str else: black_dir_str = "" white_dir_str = "" self.channel.broadcast_cc(self.prefix + "^KBlack%s^~: ^R%s^~; ^WWhite%s^~: ^Y%s^~\n" % (black_dir_str, self.seats[0].player, white_dir_str, self.seats[1].player)) self.turn = BLACK self.turn_number = 1 self.send_board() def set_size(self, player, size_bits): # Is there an 'x' in the middle of a single argument? if len(size_bits) == 1: size_bits[0] = size_bits[0].lower() if "x" in size_bits[0]: size_bits = size_bits[0].split("x") width = size_bits[0] # If there's a single element, height == width. if len(size_bits) == 1: height = width elif len(size_bits) == 2: width = size_bits[0] height = size_bits[1] else: player.tell_cc(self.prefix + "Invalid size command.\n") return if not width.isdigit() or not height.isdigit(): player.tell_cc(self.prefix + "Invalid size command.\n") return w = int(width) h = int(height) if w < MIN_SIZE or w > MAX_SIZE or h < MIN_SIZE or h > MAX_SIZE: player.tell_cc(self.prefix + "Width and height must be between %d and %d inclusive.\n" % (MIN_SIZE, MAX_SIZE)) return # We disallow uneven boards if we have directional goals. if self.directional and w != h: player.tell_cc(self.prefix + "Directional games must have square boards.\n") return # Valid! self.goban.resize(w, h) self.channel.broadcast_cc(self.prefix + "^R%s^~ has set the board size to ^C%d^Gx^C%d^~.\n" % (player, w, h)) def set_directional(self, player, dir_bits): dir_bool = booleanize(dir_bits) if dir_bool: if dir_bool > 0: if self.goban.height != self.goban.width: player.tell_cc(self.prefix + "Cannot change to directional with uneven sides. Resize first.\n") return self.directional = True display_str = "^Con^~" elif dir_bool < 0: self.directional = False display_str = "^coff^~" self.channel.broadcast_cc(self.prefix + "^R%s^~ has turned directional goals %s.\n" % (player, display_str)) else: player.tell_cc(self.prefix + "Not a valid boolean!\n") def resign(self, player): seat = self.get_seat_of_player(player) if not seat: player.tell_cc(self.prefix + "You can't resign; you're not playing!\n") return False if self.turn != seat.data.side: player.tell_cc(self.prefix + "You must wait for your turn to resign.\n") return False self.resigner = seat.data.side self.channel.broadcast_cc(self.prefix + "^R%s^~ is resigning from the game.\n" % player) return True def move(self, player, move): seat = self.get_seat_of_player(player) if not seat: player.tell_cc(self.prefix + "You can't move; you're not playing!\n") return False if self.turn != seat.data.side: player.tell_cc(self.prefix + "You must wait for your turn to move.\n") return False # Check bounds. col, row = move if row < 0 or row >= self.goban.height or col < 0 or col >= self.goban.width: player.tell_cc(self.prefix + "Your move is out of bounds.\n") return False # Check that the space is empty. if self.goban.board[row][col]: player.tell_cc(self.prefix + "That space is already occupied.\n") return False # Is this move suicidal? If so, it can't be played. if self.goban.move_is_suicidal(seat.data.side, row, col): player.tell_cc(self.prefix + "That move is suicidal.\n") return False # Does this move cause a repeat of a previous board? if self.goban.move_causes_repeat(seat.data.side, row, col): player.tell_cc(self.prefix + "That move causes a repeat of a previous board.\n") return False # Okay, this looks like a legitimate move. move_return = self.goban.go_play(seat.data.side, row, col, suicide_is_valid=False) if not move_return: player.tell_cc(self.prefix + "That move was unsuccessful. Weird.\n") return False else: coords, capture_color, capture_list = move_return move_str = "%s%s" % (LETTERS[col], row + 1) capture_str = "" if capture_color: # Captured opponent pieces! capture_str += ", ^!capturing %s^." % (self.get_stone_str(len(capture_list))) # And no matter what, print information about the move. self.channel.broadcast_cc(self.prefix + "^Y%s^~ places a stone at ^C%s^~%s.\n" % (player, move_str, capture_str)) self.turn_number += 1 return True def swap(self, player): self.goban.invert(self.directional) self.channel.broadcast_cc(self.prefix + "^Y%s^~ has swapped ^KBlack^~'s first move.\n" % (player)) self.turn_number += 1 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.split() primary = command_bits[0] if state == "setup": if primary in ("size", "sz",): self.set_size(player, command_bits[1:]) handled = True if primary in ("directional", "goals", "dir", "goal",): self.set_directional(player, command_bits[1]) handled = True if primary in ("done", "ready", "d", "r",): self.channel.broadcast_cc(self.prefix + "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.channel.broadcast_cc(self.prefix + "^R%s^~ has switched the game to setup mode.\n" % player) handled = True elif state == "playing": made_move = False if primary in ("move", "play", "mv", "pl",): invalid = False move_bits = demangle_move(command_bits[1:]) if move_bits and len(move_bits) == 1: made_move = self.move(player, move_bits[0]) else: invalid = True if invalid: player.tell_cc(self.prefix + "Invalid move command.\n") handled = True elif primary in ("swap",): if self.turn_number == 2 and self.seats[1].player == player: self.swap(player) made_move = True else: player.tell_cc(self.prefix + "Unsuccessful swap.\n") handled = True elif primary in ("resign",): if self.resign(player): made_move = True handled = True if made_move: if self.turn == BLACK: self.turn = WHITE else: self.turn = BLACK # Did someone win? winner = self.find_winner() if winner: self.resolve(winner) self.finish() else: # Nope. show everyone the board, and keep on. self.send_board() if not handled: player.tell_cc(self.prefix + "Invalid command.\n") def reset_adjacency(self): self.adjacency_map = [] for r in range(self.goban.height): self.adjacency_map.append([None] * self.goban.width) def find_winner(self): # If someone resigned, this is the easiest thing ever. if self.resigner == WHITE: return self.seats[0].player_name elif self.resigner == BLACK: return self.seats[1].player_name # Okay, we have to check the board. First, determine which # checks we need to make. In a directional game, we only # need to test the left and top edges for White and Black # respectively; otherwise we need to test both edges for # both players. self.found_winner = False self.reset_adjacency() for r in range(self.goban.height): self.recurse_adjacencies(WHITE, r, 0, TEST_RIGHT) if not self.found_winner: for c in range(self.goban.width): self.recurse_adjacencies(BLACK, 0, c, TEST_DOWN) if not self.found_winner and not self.directional: # Gotta test both edges with the other colors. Reset the # adjacency graph, as the previous entries will now conflict. self.reset_adjacency() for r in range(self.goban.height): self.recurse_adjacencies(BLACK, r, 0, TEST_RIGHT) if not self.found_winner: for c in range(self.goban.width): self.recurse_adjacencies(WHITE, 0, c, TEST_DOWN) if self.found_winner == BLACK: return self.seats[0].player_name elif self.found_winner == WHITE: return self.seats[1].player_name # Blarg, still no winner. See if the next player (we've already # switched turns) has no valid moves. If so, the current player # wins. for r in range(self.goban.height): for c in range(self.goban.width): if (not self.goban.board[r][c] and not self.goban.move_is_suicidal(self.turn, r, c) and not self.goban.move_causes_repeat(self.turn, r, c)): # Player has a non-suicidal move. No winner. return None # Checked all valid moves for the next player, and they're all # suicidal. This player wins. if self.turn == WHITE: return self.seats[0].player_name else: return self.seats[1].player_name def recurse_adjacencies(self, color, row, col, test_dir): # Bail if a winner's been found. if self.found_winner: return # Bail if we're off the board. if (row < 0 or row >= self.goban.height or col < 0 or col >= self.goban.width): return # Bail if we've visited this location. if self.adjacency_map[row][col]: return # Bail if it's the wrong color. if self.goban.board[row][col] != color: return # Okay, it's the right color. Mark it visited... self.adjacency_map[row][col] = True # Have we reached the proper side? if ((test_dir == TEST_RIGHT and col == self.goban.width - 1) or (test_dir == TEST_DOWN and row == self.goban.height - 1)): # Winner! self.found_winner = color return # Not a win yet... so we need to test the four adjacencies. for r_delta, c_delta in SQUARE_DELTAS: self.recurse_adjacencies(color, row + r_delta, col + c_delta, test_dir) def resolve(self, winner): self.send_board() self.channel.broadcast_cc(self.prefix + "^C%s^~ wins!\n" % winner) def show_help(self, player): super(Gonnect, self).show_help(player) player.tell_cc("\nGONNECT SETUP PHASE:\n\n") player.tell_cc(" ^!setup^., ^!config^., ^!conf^. Enter setup phase.\n") player.tell_cc(" ^!size^. <size> | <w> <h>, ^!sz^. Set board to <size>x<size>/<w>x<h>.\n") player.tell_cc(" ^!directional^. off|on, ^!dir^. Turn directional goals off|on.\n") player.tell_cc(" ^!ready^., ^!done^., ^!r^., ^!d^. End setup phase.\n") player.tell_cc("\nGONNECT PLAY:\n\n") player.tell_cc(" ^!move^. <ln>, ^!mv^. Place stone at <ln> (letter number).\n") player.tell_cc(" ^!swap^. Swap first move (White only, first only).\n") player.tell_cc(" ^!resign^. Resign.\n")
class Ataxx(SeatedGame): """An Ataxx game table implementation. Invented in 1988 by Dave Crummack and Craig Galley. """ def __init__(self, server, table_name): super(Ataxx, self).__init__(server, table_name) self.game_display_name = "Ataxx" self.game_name = "ataxx" self.seats = [ Seat("Red"), Seat("Blue"), ] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.prefix = "(^RAtaxx^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Ataxx-specific stuff. self.board = None self.printable_board = None self.sides = {} self.size = 7 self.player_mode = 2 self.turn = None self.last_r = None self.last_c = None self.init_seats() self.init_board() def init_board(self): self.board = [] for r in range(self.size): self.board.append([None] * self.size) # Place starting pieces, depending on the number of players. bottom_left = BLUE bottom_right = RED if self.player_mode == 4: bottom_left = YELLOW bottom_right = GREEN self.board[0][0] = RED self.board[0][self.size - 1] = BLUE self.board[self.size - 1][0] = bottom_left self.board[self.size - 1][self.size - 1] = bottom_right self.update_printable_board() def init_seats(self): # If we're in 2-player mode, and there are 4 seats, delete the # extras. if self.player_mode == 2 and len(self.seats) == 4: del self.seats[2] del self.seats[2] self.sides = {} # Set the sides and data for players one and two. self.seats[0].data.side = RED self.seats[0].data.count = 2 self.seats[0].data.resigned = False self.seats[1].data.side = BLUE self.seats[1].data.count = 2 self.seats[1].data.resigned = False self.sides[RED] = self.seats[0] self.sides[BLUE] = self.seats[1] # If there are four players... if self.player_mode == 4: # ...and only two seats, create them. if len(self.seats) == 2: self.seats.append(Seat("Green")) self.seats.append(Seat("Yellow")) # Either way, set the sides and data. self.seats[2].data.side = GREEN self.seats[2].data.resigned = False self.sides[GREEN] = self.seats[2] self.seats[3].data.side = YELLOW self.seats[3].data.resigned = False self.sides[YELLOW] = self.seats[3] self.seats[0].data.count = 1 self.seats[1].data.count = 1 self.seats[2].data.count = 1 self.seats[3].data.count = 1 def change_player_mode(self, count): # Don't bother if it's the mode we're already in. if count == self.player_mode: return False # Don't bother if it's not a valid option either. if count != 2 and count != 4: return False # Okay. Set values... self.player_mode = count self.min_players = count self.max_players = count # ...initialize the seats... self.init_seats() # ...and reinitialize the board. self.init_board() def update_printable_board(self): self.printable_board = [] col_str = " " + "".join([" " + COLS[i] for i in range(self.size)]) self.printable_board.append(col_str + "\n") self.printable_board.append(" ^m.=" + "".join(["=="] * self.size) + ".^~\n") for r in range(self.size): this_str = "%2d ^m|^~ " % (r + 1) for c in range(self.size): if r == self.last_r and c == self.last_c: this_str += "^I" loc = self.board[r][c] if loc == RED: this_str += "^RR^~ " elif loc == BLUE: this_str += "^BB^~ " elif loc == GREEN: this_str += "^GG^~ " elif loc == YELLOW: this_str += "^YY^~ " elif loc == PIT: this_str += "^Ko^~ " else: this_str += "^M.^~ " this_str += "^m|^~ %d" % (r + 1) self.printable_board.append(this_str + "\n") self.printable_board.append(" ^m`=" + "".join(["=="] * self.size) + "'^~\n") self.printable_board.append(col_str + "\n") def get_info_str(self): if not self.turn: return ("The game has not yet started.\n") if self.turn == RED: name = self.seats[0].player_name turn_str = "^RRed^~" elif self.turn == BLUE: name = self.seats[1].player_name turn_str = "^BBlue^~" elif self.turn == GREEN: name = self.seats[2].player_name turn_str = "^GGreen^~" else: name = self.seats[3].player_name turn_str = "^YYellow^~" info_str = "It is %s's turn (%s).\n" % (name, turn_str) info_str += "^RRed^~: %d ^BBlue^~: %d" % (self.seats[0].data.count, self.seats[1].data.count) if self.player_mode == 4: info_str += " ^GGreen^~: %d ^YYellow^~: %d" % ( self.seats[2].data.count, self.seats[3].data.count) info_str += "\n" return (info_str) def show(self, player): if not self.printable_board: self.update_printable_board() for line in self.printable_board: player.tell_cc(line) player.tell_cc(self.get_info_str()) def send_board(self): for listener in self.channel.listeners: self.show(listener) def is_valid(self, row, col): # Note that this does /not/ care about pits, just about the proper # ranges for coordinates. if row < 0 or row >= self.size or col < 0 or col >= self.size: return False return True def piece_has_move(self, row, col): # Returns whether or not a given piece has a potential move. # Bail on dud data. if not self.is_valid(row, col) or not self.board[row][col]: return False # Okay. A piece can potentially move anywhere in a 5x5 area centered # on its location. found_move = False for r_d in range(-2, 3): # <--- why I hate range syntax. for c_d in range(-2, 3): if not found_move and (self.is_valid(row + r_d, col + c_d) and not self.board[row + r_d][col + c_d]): found_move = True # Return whether we found a move or not. return found_move def color_has_move(self, color): # Returns whether or not a given side has a potential move. # Bail immediately if green or yellow and we're in 2p mode. if self.player_mode == 2 and (color == YELLOW or color == GREEN): return False # Bail if this player has resigned. if ((color == RED and self.seats[0].data.resigned) or (color == BLUE and self.seats[1].data.resigned) or (color == GREEN and self.seats[2].data.resigned) or (color == YELLOW and self.seats[3].data.resigned)): return False # Okay. Scan the board for pieces... for r in range(self.size): for c in range(self.size): if self.board[r][c] == color and self.piece_has_move(r, c): return True # Found no moves. This color has no valid moves. return False def loc_to_str(self, row, col): return "%s%s" % (COLS[col], row + 1) def move(self, player, src_loc, dst_loc): seat = self.get_seat_of_player(player) if not seat: player.tell_cc(self.prefix + "You can't move; you're not playing!\n") return False if self.turn != seat.data.side: player.tell_cc(self.prefix + "You must wait for your turn to move.\n") return False if src_loc == dst_loc: player.tell_cc(self.prefix + "You can't make a non-move move!\n") return False src_c, src_r = src_loc dst_c, dst_r = dst_loc if not self.is_valid(src_c, src_r) or not self.is_valid(dst_c, dst_r): player.tell_cc(self.prefix + "Your move is out of bounds.\n") return False src_str = self.loc_to_str(src_r, src_c) dst_str = self.loc_to_str(dst_r, dst_c) # Do they have a piece at the source? color = seat.data.side if self.board[src_r][src_c] != color: player.tell_cc(self.prefix + "You don't have a piece at ^C%s^~.\n" % src_str) return False # Is the destination within range? if abs(src_r - dst_r) > 2 or abs(src_c - dst_c) > 2: player.tell_cc(self.prefix + "That move is too far.\n") return False # Is the destination empty? if self.board[dst_r][dst_c]: player.tell_cc(self.prefix + "^C%s^~ is already occupied.\n" % dst_str) return False # In range, to an empty cell. It's a valid move. Mark it. self.last_r = dst_r self.last_c = dst_c # Now, is it a split or a leap? if abs(src_r - dst_r) < 2 and abs(src_c - dst_c) < 2: # Split. Add a new piece, increase the count. action_str = "^Mgrew^~ into" self.board[dst_r][dst_c] = color seat.data.count += 1 else: # Leap. Move the piece, don't increase the count. action_str = "^Cjumped^~ to" self.board[src_r][src_c] = None self.board[dst_r][dst_c] = color # Whichever action occurred, check all cells surrounding the # destination. If they are opponents, transform them. change_count = 0 change_str = "" for r_d in range(-1, 2): for c_d in range(-1, 2): if self.is_valid(dst_r + r_d, dst_c + c_d): occupier = self.board[dst_r + r_d][dst_c + c_d] if occupier and occupier != color and occupier != PIT: # Another player. Uh oh! Flip it and decrement that # player's count. self.board[dst_r + r_d][dst_c + c_d] = color seat.data.count += 1 self.sides[occupier].data.count -= 1 change_count += 1 if change_count: change_str = ", ^!converting %d piece" % change_count if change_count != 1: change_str += "s" # Tell everyone what just happened. self.channel.broadcast_cc( self.prefix + "From ^c%s^~, %s %s ^C%s^~%s^~.\n" % (src_str, player, action_str, dst_str, change_str)) self.update_printable_board() return True def toggle_pits(self, player, loc_list): # Undocumented bonus feature: handles multiple locations, but if # any of them are invalid, it'll bail halfway through. Useful for # prepping a particular cool layout with a single cut-and-pasted # string, though. for loc in loc_list: col, row = loc # Bail if out of bounds. if not self.is_valid(row, col): player.tell_cc(self.prefix + "Pit out of bounds.\n") return # Bail if a starting piece is there. thing_there = self.board[row][col] if thing_there and not (thing_there == PIT): player.tell_cc(self.prefix + "Cannot put a pit on a starting piece.\n") return # Since it's a toggle, figure out what we're toggling to. if thing_there: new_thing = None action_str = "^cremoved^~" else: new_thing = PIT action_str = "^Cadded^~" # Tentative place the thing. self.board[row][col] = new_thing # Does it keep red or blue (which, in a 4p game, is equivalent to # all four players) from being able to make a move? If so, it's # invalid. Put the board back the way it was. if not self.color_has_move(RED) or not self.color_has_move(BLUE): player.tell_cc(self.prefix + "Players must have a valid move.\n") self.board[row][col] = thing_there return loc_list = [(row, col)] edge = self.size - 1 # In either mode, we place another pit across the center line, # but not if that's the same location as the one we just placed # (on the center line on odd-sized boards). if (edge - row) != row: self.board[edge - row][col] = new_thing loc_list.append((edge - row, col)) # Handle the 4p down-reflection if necessary. if self.player_mode == 4 and (edge - col) != col: self.board[edge - row][edge - col] = new_thing loc_list.append((edge - row, edge - col)) # Handle the 4p right-reflection if necessary. if self.player_mode == 4 and (edge - col) != col: self.board[row][edge - col] = new_thing loc_list.append((row, edge - col)) # Generate the list of locations. loc_str = ", ".join( ["^C%s^~" % self.loc_to_str(x[0], x[1]) for x in loc_list]) # Finally, send the string detailing what just happened. self.channel.broadcast_cc(self.prefix + "^Y%s^~ has %s a pit at: %s\n" % (player, action_str, loc_str)) self.update_printable_board() def set_size(self, player, size_bits): if not size_bits.isdigit(): player.tell_cc(self.prefix + "Invalid size command.\n") return size = int(size_bits) if size < MIN_SIZE or size > MAX_SIZE: player.tell_cc(self.prefix + "Size must be between %d and %d inclusive.\n" % (MIN_SIZE, MAX_SIZE)) return # Valid! self.size = size self.channel.broadcast_cc( self.prefix + "^R%s^~ has set the board size to ^C%d^~.\n" % (player, size)) self.init_board() self.update_printable_board() def set_player_mode(self, player, mode_bits): if not mode_bits.isdigit(): player.tell_cc(self.prefix + "Invalid player mode command.\n") return mode = int(mode_bits) if mode != 2 and mode != 4: player.tell_cc(self.prefix + "This game only supports two or four players.\n") return elif mode == self.player_mode: player.tell_cc(self.prefix + "This table is already in that mode.\n") return else: self.change_player_mode(mode) self.channel.broadcast_cc( self.prefix + "^Y%s^~ has changed the game to ^C%d-player^~ mode.\n" % (player, mode)) def resign(self, player): seat = self.get_seat_of_player(player) if not seat: player.tell_cc(self.prefix + "You can't resign; you're not playing!\n") return False if self.turn != seat.data.side: player.tell_cc(self.prefix + "You must wait for your turn to resign.\n") return False if seat.data.resigned: player.tell_cc(self.prefix + "You've already resigned.\n") return False # They've passed the tests and can resign. seat.data.resigned = True self.channel.broadcast_cc(self.prefix + "^R%s^~ is resigning from the game.\n" % player) def tick(self): # If all seats are full and the game is active, autostart. active_seats = [x for x in self.seats if x.player] if (self.state.get() == "need_players" and len(active_seats) == self.player_mode and self.active): self.state.set("playing") send_str = "^RRed^~: %s; ^BBlue^~: %s" % ( self.seats[0].player_name, self.seats[1].player_name) if self.player_mode == 4: send_str += "; ^GGreen^~: %s; ^YYellow^~: %s" % ( self.seats[2].player_name, self.seats[3].player_name) self.channel.broadcast_cc(self.prefix + send_str + "\n") self.turn = RED self.send_board() 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.split() primary = command_bits[0].lower() if state == "setup": if primary in ( "size", "sz", ): if len(command_bits) == 2: self.set_size(player, command_bits[1]) else: player.tell_cc(self.prefix + "Invalid size command.\n") handled = True elif primary in ( "players", "player", "pl", ): if len(command_bits) == 2: self.set_player_mode(player, command_bits[1]) else: player.tell_cc(self.prefix + "Invalid player mode command.\n") handled = True elif primary in ( "pit", "hole", ): loc_list = demangle_move(command_bits[1:]) if loc_list: self.toggle_pits(player, loc_list) else: player.tell_cc(self.prefix + "Invalid pit command.\n") handled = True elif primary in ( "ready", "done", "r", "d", ): self.channel.broadcast_cc( self.prefix + "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.channel.broadcast_cc( self.prefix + "^R%s^~ has switched the game to setup mode.\n" % player) handled = True elif state == "playing": made_move = False if primary in ( "move", "play", "mv", "pl", ): move_bits = demangle_move(command_bits[1:]) if move_bits and len(move_bits) == 2: made_move = self.move(player, move_bits[0], move_bits[1]) else: player.tell_cc(self.prefix + "Invalid move command.\n") handled = True elif primary in ("resign", ): self.resign(player) made_move = True handled = True if made_move: # Did someone win? winner = self.find_winner() if winner: self.resolve(winner) self.finish() else: # Okay, well, let's see whose turn it is. If it comes # back around to us, the game is over anyway. curr_turn = self.turn done = False while not done: if self.turn == RED: self.turn = BLUE elif self.turn == BLUE: # The only tough one; switch depending on mode. if self.player_mode == 2: self.turn = RED else: self.turn = GREEN elif self.turn == GREEN: self.turn = YELLOW elif self.turn == YELLOW: self.turn = RED # Now see if this player even has a move. if self.color_has_move(self.turn): done = True elif self.turn == curr_turn: # If we've wrapped back around to the current # turn, no one had a move. Bail as well. done = True # Check to see if we're back at the mover. if curr_turn == self.turn: # No one had a valid move. Game's over. self.no_move_resolve() self.finish() else: # Otherwise it's some other player's turn; game on. self.send_board() if not handled: player.tell_cc(self.prefix + "Invalid command.\n") def find_winner(self): # Get the list of players that haven't resigned and have at least one # piece left on the board. If that list is only one long, we have a # winner. Otherwise, the game continues. live_players = [ x for x in self.seats if ((not x.data.resigned) and x.data.count) ] if len(live_players) == 1: return live_players[0].player_name else: return None def resolve(self, winner): self.send_board() self.channel.broadcast_cc(self.prefix + "^C%s^~ wins!\n" % winner) def no_move_resolve(self): self.send_board() # We look at the number of pieces each player has. Highest wins. high_count = -1 high_list = None for seat in self.seats: if seat.data.count > high_count: high_count = seat.data.count high_list = ["^C%s^~" % seat.player_name] elif seat.data.count == high_count: # Potential tie. high_list.append("^C%s^~" % seat.player_name) # If a single player has the highest count, they win; otherwise, tie. if len(high_list) == 1: self.channel.broadcast_cc(self.prefix + "%s wins with ^Y%d^~ pieces!\n" % (high_list[0], high_count)) else: self.channel.broadcast_cc( self.prefix + "These players ^Rtied^~ for first with ^Y%d^~ pieces: %s\n" % (", ".join(high_list))) def show_help(self, player): super(Ataxx, self).show_help(player) player.tell_cc("\nATAXX SETUP PHASE:\n\n") player.tell_cc( " ^!setup^., ^!config^., ^!conf^. Enter setup phase.\n" ) player.tell_cc( " ^!size^. <size>, ^!sz^. Set board to <size>.\n") player.tell_cc( " ^!players^. 2|4, ^!pl^. Set number of players.\n" ) player.tell_cc( " ^!pit^. <ln> Add or remove pit at <ln>.\n" ) player.tell_cc( " ^!ready^., ^!done^., ^!r^., ^!d^. End setup phase.\n" ) player.tell_cc("\nATAXX PLAY:\n\n") player.tell_cc( " ^!move^. <ln> <ln2>, ^!mv^. Move from <ln> to <ln2> (letter number).\n" ) player.tell_cc(" ^!resign^. Resign.\n")
class Whist(SeatedGame): """A Whist game table implementation. Whist came about sometime in the 18th century. This implementation does not (currently) score honours, because honours are boring. """ def __init__(self, server, table_name): super(Whist, self).__init__(server, table_name) self.game_display_name = "Whist" self.game_name = "whist" self.seats = [ Seat("North"), Seat("East"), Seat("South"), Seat("West"), ] self.min_players = 4 self.max_players = 4 self.state = State("need_players") self.prefix = "(^RWhist^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Whist-specific guff. self.ns = Struct() self.ns.score = 0 self.ew = Struct() self.ew.score = 0 self.seats[0].data.who = NORTH self.seats[1].data.who = EAST self.seats[2].data.who = SOUTH self.seats[3].data.who = WEST self.goal = 5 self.trick = None self.trump_suit = None self.led_suit = None self.turn = None self.dealer = None self.winner = None self.layout = FourPlayerCardGameLayout() def show_help(self, player): super(Whist, self).show_help(player) player.tell_cc("\nWHIST SETUP PHASE:\n\n") player.tell_cc( " ^!setup^., ^!config^., ^!conf^. Enter setup phase.\n" ) player.tell_cc( " ^!goal^. <num>, ^!score^. Set the goal score to <num>.\n" ) player.tell_cc( " ^!ready^., ^!done^., ^!r^., ^!d^. End setup phase.\n" ) player.tell_cc("\nWHIST PLAY:\n\n") player.tell_cc( " ^!play^. <card>, ^!pl^. Play <card> from your hand.\n" ) player.tell_cc( " ^!hand^., ^!inv^., ^!i^. Look at the cards in your hand.\n" ) def display(self, player): player.tell_cc("%s" % self.layout) def get_score_str(self): return " ^RNorth/South^~: %d ^MEast/West^~: %d\n" % ( self.ns.score, self.ew.score) def get_color_code(self, seat): if seat == self.seats[0] or seat == self.seats[2]: return "^R" else: return "^M" def get_sp_str(self, seat): return "^G%s^~ (%s%s^~)" % (seat.player_name, self.get_color_code(seat), seat) def get_metadata(self): to_return = "\n\n" if self.turn: to_return += "It is ^Y%s^~'s turn (%s%s^~). Trumps are ^C%s^~.\n" % ( self.turn.player_name, self.get_color_code( self.turn), self.turn, self.trump_suit) to_return += "Tricks: ^RNorth/South^~: %d ^MEast/West^~: %d\n" % ( self.ns.tricks, self.ew.tricks) to_return += "The goal score for this game is ^C%s^~.\n" % get_plural_str( self.goal, "point") to_return += self.get_score_str() return to_return def show(self, player): self.display(player) player.tell_cc(self.get_metadata()) 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 clear_trick(self): # Set the current trick to an empty hand... self.trick = Hand() self.led_suit = None # ...and set everyone's played card to None. for seat in self.seats: seat.data.card = None # Clear the layout as well. self.layout.clear() def new_deal(self): dealer_name = self.dealer.player_name self.bc_pre( "^R%s^~ (%s%s^~) gives the cards a good shuffle...\n" % (dealer_name, self.get_color_code(self.dealer), self.dealer)) deck = new_deck() deck.shuffle() # Deal out all of the cards. We'll flip the last one; that determines # the trump suit for the hand. self.bc_pre("^R%s^~ deals the cards out to all the players.\n" % dealer_name) for seat in self.seats: seat.data.hand = Hand() for i in range(13): for seat in self.seats: seat.data.hand.add(deck.discard()) # Flip the dealer's last card; it determines the trump suit. last_card = self.dealer.data.hand[-1] self.bc_pre("^R%s^~ flips their last card; it is ^C%s^~.\n" % (dealer_name, card_to_str(last_card, LONG))) self.trump_suit = last_card.suit # Sort everyone's hands. for seat in self.seats: seat.data.hand = sorted_hand(seat.data.hand, self.trump_suit) # Show everyone their hands. self.show_hands() # Set the trick counts to zero. self.ns.tricks = 0 self.ew.tricks = 0 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, self.trump_suit) print_str += "\n" self.tell_pre(player, print_str) def show_hands(self): for seat in self.seats: if seat.player: self.show_hand(seat.player) 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 # 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 # Okay, it's a card in their hand. First, let's do the "follow the # led suit" business. action_str = "^Wplays^~" if self.led_suit: this_suit = potential_card.suit if (this_suit != self.led_suit and hand_has_suit(seat.data.hand, self.led_suit)): # You can't play off-suit if you can match the led suit. self.tell_pre(player, "You can't throw off; you have the led suit.\n") return False else: # No led suit; they're the leader. action_str = "^Yleads^~ with" self.led_suit = potential_card.suit # They either matched the led suit, didn't have any of it, or they # are themselves the leader. Nevertheless, their play is valid. seat.data.card = potential_card self.trick.add(seat.data.hand.discard_specific(potential_card)) trump_str = "" if potential_card.suit == self.trump_suit: trump_str = ", a ^Rtrump^~" self.bc_pre("%s %s ^C%s^~%s.\n" % (self.get_sp_str(seat), action_str, card_to_str(potential_card, LONG), trump_str)) self.layout.place(seat.data.who, potential_card) return potential_card def tick(self): # If all seats are full and active, autostart. if (self.state.get() == "need_players" and self.seats[0].player and self.seats[1].player and self.seats[2].player and self.seats[3].player and self.active): self.state.set("playing") self.bc_pre("The game has begun.\n") # Initialize everything by clearing the (non-existent) trick. self.clear_trick() # Make a new deal. self.dealer = self.seats[0] self.new_deal() # Eldest leads to the first trick. self.turn = self.next_seat(self.dealer) self.layout.change_turn(self.turn.data.who) 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.split() primary = command_bits[0].lower() if state == "setup": if primary in ( "goal", "score", "sc", "g", ): 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": card_played = False if primary in ( "hand", "inventory", "inv", "i", ): self.show_hand(player) handled = True elif primary in ( "play", "move", "pl", "mv", ): if len(command_bits) == 2: card_played = self.play(player, command_bits[1]) else: self.tell_pre(player, "Invalid play command.\n") handled = True if card_played: # A card hit the table. We need to do stuff. if len(self.trick) == 4: # Finish the trick up. self.finish_trick() # Is that the last trick of this hand? if self.ns.tricks + self.ew.tricks == 13: # Yup. Finish the hand up. self.finish_hand() # Did someone win the overall game? winner = self.find_winner() if winner: # Yup. Finish. self.resolve(winner) self.finish() else: # Nope. Pass the deal to the next dealer... self.dealer = self.next_seat(self.dealer) # Deal and set up the first player. self.new_deal() self.turn = self.next_seat(self.dealer) self.layout.change_turn(self.turn.data.who) else: # Trick not over. Rotate. self.turn = self.next_seat(self.turn) self.layout.change_turn(self.turn.data.who) if self.turn.player: self.show_hand(self.turn.player) if not handled: self.tell_pre(player, "Invalid command.\n") def finish_trick(self): # Okay, we have a trick with four cards. Which card won? winner = handle_trick(self.trick, self.trump_suit) # This /should/ just return one seat... winning_seat_list = [x for x in self.seats if x.data.card == winner] if len(winning_seat_list) != 1: self.server.log.log( self.log_prefix + "Something went horribly awry; trick ended without a finish.") self.bc_pre( "Something went horribly wrong; no one won the trick! Tell the admin.\n" ) return winning_seat = winning_seat_list[0] # Print information about the winning card. self.bc_pre("%s wins the trick with ^C%s^~.\n" % (self.get_sp_str(winning_seat), card_to_str(winner, LONG))) # Give the trick to the correct partnership. if winning_seat == self.seats[0] or winning_seat == self.seats[2]: self.ns.tricks += 1 else: self.ew.tricks += 1 # Clear the trick. self.clear_trick() # Set the next leader to the player who won. self.turn = winning_seat self.layout.change_turn(self.turn.data.who) if self.turn.player: self.show_hand(self.turn.player) def finish_hand(self): # Which side won more than 6 tricks? if self.ns.tricks > 6: winning_side = "^RNorth/South^~" addend = self.ns.tricks - 6 self.ns.score += addend else: winning_side = "^MEast/West^~" addend = self.ew.tricks - 6 self.ew.score += addend # Let everyone know. self.bc_pre("%s wins the hand and gains ^C%s^~.\n" % (winning_side, get_plural_str(addend, "point"))) self.bc_pre(self.get_score_str()) def find_winner(self): # Easy: has one of the sides reached a winning score? if self.ns.score >= self.goal: return self.ns elif self.ew.score >= self.goal: return self.ew return None def resolve(self, winning_partnership): if self.ns == winning_partnership: name_one = self.seats[0].player_name name_two = self.seats[2].player_name else: name_one = self.seats[1].player_name name_two = self.seats[3].player_name self.bc_pre("^G%s^~ and ^G%s^~ win!\n" % (name_one, name_two))
class Metamorphosis(SeatedGame): """A Metamorphosis game table implementation. Invented in 2009 by Gregory Keith Van Patten. Play seems to show that ko fight mode is definitely superior to the alternative, so we set it as default. """ def __init__(self, server, table_name): super(Metamorphosis, self).__init__(server, table_name) self.game_display_name = "Metamorphosis" self.game_name = "metamorphosis" self.seats = [ Seat("Black"), Seat("White") ] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.prefix = "(^RMetamorphosis^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Metamorphosis-specific stuff. self.board = None self.printable_board = None self.size = 12 self.ko_fight = True self.group_count = None self.turn = None self.turn_number = 0 self.seats[0].data.side = BLACK self.seats[0].data.last_was_ko = False self.seats[1].data.side = WHITE self.seats[1].data.last_was_ko = False self.last_r = None self.last_c = None self.resigner = None self.adjacency_map = None self.found_winner = False self.init_board() def init_board(self): self.board = [] # Generate a new empty board. Boards alternate the starting checkered # layout, which we can pregen. white_first_row = [] black_first_row = [] for c in range(self.size): if c % 2: white_first_row.append(BLACK) black_first_row.append(WHITE) else: white_first_row.append(WHITE) black_first_row.append(BLACK) # Then we just add the appropriate rows, two at a time. for r in range(self.size / 2): self.board.append(white_first_row[:]) self.board.append(black_first_row[:]) # Count the number of groups on the board. Should be size^2. self.group_count = self.get_group_count() def update_printable_board(self): self.printable_board = [] col_str = " " + "".join([" " + COLS[i] for i in range(self.size)]) self.printable_board.append(col_str + "\n") self.printable_board.append(" ^m.=" + "".join(["=="] * self.size) + ".^~\n") for r in range(self.size): this_str = "%2d ^m|^~ " % (r + 1) for c in range(self.size): if r == self.last_r and c == self.last_c: this_str += "^5" loc = self.board[r][c] if loc == WHITE: this_str += "^Wo^~ " elif loc == BLACK: this_str += "^Kx^~ " else: this_str += "^M.^~ " this_str += "^m|^~ %d" % (r + 1) self.printable_board.append(this_str + "\n") self.printable_board.append(" ^m`=" + "".join(["=="] * self.size) + "'^~\n") self.printable_board.append(col_str + "\n") def show(self, player): if not self.printable_board: self.update_printable_board() for line in self.printable_board: player.tell_cc(line) player.tell_cc(self.get_turn_str() + "\n") def send_board(self): for player in self.channel.listeners: self.show(player) def get_turn_str(self): if not self.turn: return ("The game has not yet started.\n") if self.turn == BLACK: player = self.seats[0].player_name color_msg = "^KBlack/Vertical^~" else: player = self.seats[1].player_name color_msg = "^WWhite/Horizontal^~" return ("It is ^Y%s^~'s turn (%s)." % (player, color_msg)) def is_valid(self, row, col): if row < 0 or row >= self.size or col < 0 or col >= self.size: return False return True def get_group_count(self): # Counting groups can be done in a single pass, even if the groups # are complicated. It is done as follows: # - Check the color in a location and the locations above and to the # left. # - If it's the same color as only one of them, assign ourselves the # group ID of that piece; # - If it's the same color as /both/, and they have distinct group IDs, # reassign the higher-numbered group ID to the lower group ID, as this # piece joins those two groups; # - If it's the same color as neither, add a new group ID. # At the end, we simply count the number of distinct group IDs. # Build a temporary board tracking the IDs of each spot. id_board = [] for r in range(self.size): id_board.append([None] * self.size) id_dict = {} curr_id = 0 for r in range(self.size): for c in range(self.size): self_color = self.board[r][c] above_color = None left_color = None if r - 1 >= 0: above_color = self.board[r - 1][c] if c - 1 >= 0: left_color = self.board[r][c - 1] if self_color == above_color: # Definitely a group above. Check to see if it's the same # to the left too. group_id = id_dict[id_board[r - 1][c]] if self_color == left_color: # Same color. Are they the same group ID? left_id = id_dict[id_board[r][c - 1]] if left_id == group_id: # Yup. Use it. id_board[r][c] = group_id else: # This piece joins two previously-distinct groups. # Update the ID dict to collapse those two groups # into one and use the lower-valued one for this # space. if left_id < group_id: id_dict[group_id] = left_id id_board[r][c] = left_id else: id_dict[left_id] = group_id id_board[r][c] = group_id else: # Different color left, same color above. Use the # above ID. id_board[r][c] = group_id elif self_color == left_color: # Group to the left, but not above. Use the left ID. id_board[r][c] = id_dict[id_board[r][c - 1]] else: # No group in either direction. New group. id_board[r][c] = curr_id id_dict[curr_id] = curr_id curr_id += 1 # Now that we're done with those shenanigans, count the number of unique # groups on the board. We may have to chase pointers; imagine a case # where group C coalesced into group B, and later on group B coalesced # into group A. In that case, C will still refer to group B, which no # longer exists. At the end of any coalesce chain, though, a group must # point to itself. unique_set = set() for group_id in id_dict: chase_id = id_dict[group_id] while chase_id != id_dict[chase_id]: chase_id = id_dict[chase_id] if id_dict[chase_id] not in unique_set: unique_set.add(id_dict[chase_id]) return len(unique_set) def flip(self, row, col): curr = self.board[row][col] if curr == BLACK: self.board[row][col] = WHITE else: self.board[row][col] = BLACK def move(self, player, play): seat = self.get_seat_of_player(player) if not seat: player.tell_cc(self.prefix + "You can't move; you're not playing!\n") return False if self.turn != seat.data.side: player.tell_cc(self.prefix + "You must wait for your turn to move.\n") return False col, row = play # Make sure they're all in range. if not self.is_valid(row, col): player.tell_cc(self.prefix + "Your move is out of bounds.\n") return False # Does this move increase the number of groups on the board? self.flip(row, col) new_group_count = self.get_group_count() if new_group_count > self.group_count: # Yup. Flip it back and inform the player. self.flip(row, col) player.tell_cc(self.prefix + "That move increases the group count.\n") return False move_is_ko = False ko_str = "" # If we're in ko fight mode, check to see if a ko move is valid. if new_group_count == self.group_count: move_is_ko = True ko_str = ", a ko move" if not self.ko_fight: # Flip it back; we're not in ko fight mode. self.flip(row, col) player.tell_cc(self.prefix + "That is a ko move and does not decrease the group count.\n") return False elif seat.data.last_was_ko: # Flip it back; two kos in a row is not allowed. self.flip(row, col) player.tell_cc(self.prefix + "That is a ko move and you made a ko move last turn.\n") return False elif row == self.last_r and col == self.last_c: # Flip it back; this is the same move their opponent just made. self.flip(row, col) player.tell_cc(self.prefix + "You cannot repeat your opponent's last move.\n") return False # This is a valid move. Apply, announce. play_str = "%s%s" % (COLS[col], row + 1) self.channel.broadcast_cc(self.prefix + "^Y%s^~ flips the piece at ^C%s^~%s.\n" % (seat.player, play_str, ko_str)) self.last_r = row self.last_c = col self.turn_number += 1 self.group_count = new_group_count # If it was a ko move, mark the player as having made one, so they # can't make another the next turn. Otherwise clear that bit. if move_is_ko: seat.data.last_was_ko = True else: seat.data.last_was_ko = False return True 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.channel.broadcast_cc(self.prefix + "^KBlack/Vertical^~: ^R%s^~; ^WWhite/Horizontal^~: ^Y%s^~\n" % (self.seats[0].player, self.seats[1].player)) self.turn = BLACK self.turn_number = 1 self.send_board() def set_size(self, player, size_bits): if not size_bits.isdigit(): player.tell_cc(self.prefix + "Invalid size command.\n") return size = int(size_bits) if size < MIN_SIZE or size > MAX_SIZE: player.tell_cc(self.prefix + "Size must be between %d and %d inclusive.\n" % (MIN_SIZE, MAX_SIZE)) return if size % 2: player.tell_cc(self.prefix + "Size must be even.\n") return # Valid! self.size = size self.channel.broadcast_cc(self.prefix + "^R%s^~ has set the board size to ^C%d^~.\n" % (player, size)) self.init_board() self.update_printable_board() def set_ko_fight(self, player, ko_str): ko_bool = booleanize(ko_str) if ko_bool: if ko_bool > 0: self.ko_fight = True display_str = "^Con^~" else: self.ko_fight = False display_str = "^coff^~" self.channel.broadcast_cc(self.prefix + "^R%s^~ has turned ^Gko fight^~ mode %s.\n" % (player, display_str)) def resign(self, player): seat = self.get_seat_of_player(player) if not seat: player.tell_cc(self.prefix + "You can't resign; you're not playing!\n") return False if self.turn != seat.data.side: player.tell_cc(self.prefix + "You must wait for your turn to resign.\n") return False self.resigner = seat.data.side self.channel.broadcast_cc(self.prefix + "^R%s^~ is resigning from the game.\n" % player) return True def swap(self, player): # Like Hex, a swap in Metamorphosis requires a translation to make it the # equivalent move for the other player. self.flip(self.last_r, self.last_c) self.flip(self.last_c, self.last_r) self.last_c, self.last_r = self.last_r, self.last_c self.channel.broadcast_cc("^Y%s^~ has swapped ^KBlack^~'s first move.\n" % (player)) self.turn_number += 1 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.split() primary = command_bits[0] if state == "setup": if primary in ("size", "sz",): if len(command_bits) == 2: self.set_size(player, command_bits[1]) else: player.tell_cc(self.prefix + "Invalid size command.\n") handled = True elif primary in ("ko",): if len(command_bits) == 2: self.set_ko_fight(player, command_bits[1]) else: player.tell_cc(self.prefix + "Invalid ko command.\n") handled = True if primary in ("done", "ready", "d", "r",): self.channel.broadcast_cc(self.prefix + "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.channel.broadcast_cc(self.prefix + "^R%s^~ has switched the game to setup mode.\n" % player) handled = True elif state == "playing": made_move = False if primary in ("move", "play", "mv", "pl",): invalid = False move_bits = demangle_move(command_bits[1:]) if move_bits and len(move_bits) == 1: made_move = self.move(player, move_bits[0]) else: invalid = True if invalid: player.tell_cc(self.prefix + "Invalid move command.\n") handled = True elif primary in ("swap",): if self.seats[1].player == player and self.turn_number == 2: self.swap(player) made_move = True else: player.tell_cc(self.prefix + "Invalid swap command.\n") handled = True elif primary in ("resign",): if self.resign(player): made_move = True handled = True if made_move: # Okay, something happened on the board. Update. self.update_printable_board() # Did someone win? winner = self.find_winner() if winner: self.resolve(winner) self.finish() else: # Nope. Switch turns... if self.turn == BLACK: self.turn = WHITE else: self.turn = BLACK # ...show everyone the board, and keep on. self.send_board() if not handled: player.tell_cc(self.prefix + "Invalid command.\n") def find_winner(self): # If someone resigned, this is the easiest thing ever. if self.resigner == WHITE: return self.seats[0].player_name elif self.resigner == BLACK: return self.seats[1].player_name # This is like most connection games; we check recursively from the # top and left edges to see whether a player has won. self.found_winner = False self.adjacency_map = [] for i in range(self.size): self.adjacency_map.append([None] * self.size) for i in range(self.size): if self.board[i][0] == WHITE: self.recurse_adjacency(WHITE, i, 0) if self.board[0][i] == BLACK: self.recurse_adjacency(BLACK, 0, i) # ...except that it has to be at the end of the OTHER player's turn! if self.found_winner == BLACK and self.turn == WHITE: return self.seats[0].player_name elif self.found_winner == WHITE and self.turn == BLACK: return self.seats[1].player_name # No winner yet. return None def recurse_adjacency(self, color, row, col): # Bail if we found a winner already. if self.found_winner: return # Bail if we're off the board. if not self.is_valid(row, col): return # Bail if we've been here. if self.adjacency_map[row][col]: return # Bail if this is the wrong color. if self.board[row][col] != color: return # Okay. Occupied and it's this player's. Mark. self.adjacency_map[row][col] = True # Have we hit the winning side for this player? if ((color == WHITE and col == self.size - 1) or (color == BLACK and row == self.size - 1)): # Success! self.found_winner = color return # Not a win yet. Recurse over adjacencies. for r_delta, c_delta in CONNECTION_DELTAS: self.recurse_adjacency(color, row + r_delta, col + c_delta) def resolve(self, winner): self.send_board() self.channel.broadcast_cc(self.prefix + "^C%s^~ wins!\n" % winner) def show_help(self, player): super(Metamorphosis, self).show_help(player) player.tell_cc("\nMETAMORPHOSIS SETUP PHASE:\n\n") player.tell_cc(" ^!setup^., ^!config^., ^!conf^. Enter setup phase.\n") player.tell_cc(" ^!ko^. on|off Enable/disable ko fight mode.\n") player.tell_cc(" ^!size^. <size>, ^!sz^. Set board to <size>.\n") player.tell_cc(" ^!ready^., ^!done^., ^!r^., ^!d^. End setup phase.\n") player.tell_cc("\nMETAMORPHOSIS PLAY:\n\n") player.tell_cc(" ^!move^. <ln>, ^!play^., ^!mv^., ^!pl^. Make move <ln> (letter number).\n") player.tell_cc(" ^!swap^. Swap the first move (only White, only their first).\n") player.tell_cc(" ^!resign^. Resign.\n")
class Ataxx(SeatedGame): """An Ataxx game table implementation. Invented in 1988 by Dave Crummack and Craig Galley. """ def __init__(self, server, table_name): super(Ataxx, self).__init__(server, table_name) self.game_display_name = "Ataxx" self.game_name = "ataxx" self.seats = [ Seat("Red"), Seat("Blue"), ] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.prefix = "(^RAtaxx^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Ataxx-specific stuff. self.board = None self.printable_board = None self.sides = {} self.size = 7 self.player_mode = 2 self.turn = None self.last_r = None self.last_c = None self.init_seats() self.init_board() def init_board(self): self.board = [] for r in range(self.size): self.board.append([None] * self.size) # Place starting pieces, depending on the number of players. bottom_left = BLUE bottom_right = RED if self.player_mode == 4: bottom_left = YELLOW bottom_right = GREEN self.board[0][0] = RED self.board[0][self.size - 1] = BLUE self.board[self.size - 1][0] = bottom_left self.board[self.size - 1][self.size - 1] = bottom_right self.update_printable_board() def init_seats(self): # If we're in 2-player mode, and there are 4 seats, delete the # extras. if self.player_mode == 2 and len(self.seats) == 4: del self.seats[2] del self.seats[2] self.sides = {} # Set the sides and data for players one and two. self.seats[0].data.side = RED self.seats[0].data.count = 2 self.seats[0].data.resigned = False self.seats[1].data.side = BLUE self.seats[1].data.count = 2 self.seats[1].data.resigned = False self.sides[RED] = self.seats[0] self.sides[BLUE] = self.seats[1] # If there are four players... if self.player_mode == 4: # ...and only two seats, create them. if len(self.seats) == 2: self.seats.append(Seat("Green")) self.seats.append(Seat("Yellow")) # Either way, set the sides and data. self.seats[2].data.side = GREEN self.seats[2].data.resigned = False self.sides[GREEN] = self.seats[2] self.seats[3].data.side = YELLOW self.seats[3].data.resigned = False self.sides[YELLOW] = self.seats[3] self.seats[0].data.count = 1 self.seats[1].data.count = 1 self.seats[2].data.count = 1 self.seats[3].data.count = 1 def change_player_mode(self, count): # Don't bother if it's the mode we're already in. if count == self.player_mode: return False # Don't bother if it's not a valid option either. if count != 2 and count != 4: return False # Okay. Set values... self.player_mode = count self.min_players = count self.max_players = count # ...initialize the seats... self.init_seats() # ...and reinitialize the board. self.init_board() def update_printable_board(self): self.printable_board = [] col_str = " " + "".join([" " + COLS[i] for i in range(self.size)]) self.printable_board.append(col_str + "\n") self.printable_board.append(" ^m.=" + "".join(["=="] * self.size) + ".^~\n") for r in range(self.size): this_str = "%2d ^m|^~ " % (r + 1) for c in range(self.size): if r == self.last_r and c == self.last_c: this_str += "^I" loc = self.board[r][c] if loc == RED: this_str += "^RR^~ " elif loc == BLUE: this_str += "^BB^~ " elif loc == GREEN: this_str += "^GG^~ " elif loc == YELLOW: this_str += "^YY^~ " elif loc == PIT: this_str += "^Ko^~ " else: this_str += "^M.^~ " this_str += "^m|^~ %d" % (r + 1) self.printable_board.append(this_str + "\n") self.printable_board.append(" ^m`=" + "".join(["=="] * self.size) + "'^~\n") self.printable_board.append(col_str + "\n") def get_info_str(self): if not self.turn: return("The game has not yet started.\n") if self.turn == RED: name = self.seats[0].player_name turn_str = "^RRed^~" elif self.turn == BLUE: name = self.seats[1].player_name turn_str = "^BBlue^~" elif self.turn == GREEN: name = self.seats[2].player_name turn_str = "^GGreen^~" else: name = self.seats[3].player_name turn_str = "^YYellow^~" info_str = "It is %s's turn (%s).\n" % (name, turn_str) info_str += "^RRed^~: %d ^BBlue^~: %d" % (self.seats[0].data.count, self.seats[1].data.count) if self.player_mode == 4: info_str += " ^GGreen^~: %d ^YYellow^~: %d" % (self.seats[2].data.count, self.seats[3].data.count) info_str += "\n" return(info_str) def show(self, player): if not self.printable_board: self.update_printable_board() for line in self.printable_board: player.tell_cc(line) player.tell_cc(self.get_info_str()) def send_board(self): for listener in self.channel.listeners: self.show(listener) def is_valid(self, row, col): # Note that this does /not/ care about pits, just about the proper # ranges for coordinates. if row < 0 or row >= self.size or col < 0 or col >= self.size: return False return True def piece_has_move(self, row, col): # Returns whether or not a given piece has a potential move. # Bail on dud data. if not self.is_valid(row, col) or not self.board[row][col]: return False # Okay. A piece can potentially move anywhere in a 5x5 area centered # on its location. found_move = False for r_d in range(-2, 3): # <--- why I hate range syntax. for c_d in range(-2, 3): if not found_move and (self.is_valid(row + r_d, col + c_d) and not self.board[row + r_d][col + c_d]): found_move = True # Return whether we found a move or not. return found_move def color_has_move(self, color): # Returns whether or not a given side has a potential move. # Bail immediately if green or yellow and we're in 2p mode. if self.player_mode == 2 and (color == YELLOW or color == GREEN): return False # Bail if this player has resigned. if ((color == RED and self.seats[0].data.resigned) or (color == BLUE and self.seats[1].data.resigned) or (color == GREEN and self.seats[2].data.resigned) or (color == YELLOW and self.seats[3].data.resigned)): return False # Okay. Scan the board for pieces... for r in range(self.size): for c in range(self.size): if self.board[r][c] == color and self.piece_has_move(r, c): return True # Found no moves. This color has no valid moves. return False def loc_to_str(self, row, col): return "%s%s" % (COLS[col], row + 1) def move(self, player, src_loc, dst_loc): seat = self.get_seat_of_player(player) if not seat: player.tell_cc(self.prefix + "You can't move; you're not playing!\n") return False if self.turn != seat.data.side: player.tell_cc(self.prefix + "You must wait for your turn to move.\n") return False if src_loc == dst_loc: player.tell_cc(self.prefix + "You can't make a non-move move!\n") return False src_c, src_r = src_loc dst_c, dst_r = dst_loc if not self.is_valid(src_c, src_r) or not self.is_valid(dst_c, dst_r): player.tell_cc(self.prefix + "Your move is out of bounds.\n") return False src_str = self.loc_to_str(src_r, src_c) dst_str = self.loc_to_str(dst_r, dst_c) # Do they have a piece at the source? color = seat.data.side if self.board[src_r][src_c] != color: player.tell_cc(self.prefix + "You don't have a piece at ^C%s^~.\n" % src_str) return False # Is the destination within range? if abs(src_r - dst_r) > 2 or abs(src_c - dst_c) > 2: player.tell_cc(self.prefix + "That move is too far.\n") return False # Is the destination empty? if self.board[dst_r][dst_c]: player.tell_cc(self.prefix + "^C%s^~ is already occupied.\n" % dst_str) return False # In range, to an empty cell. It's a valid move. Mark it. self.last_r = dst_r self.last_c = dst_c # Now, is it a split or a leap? if abs(src_r - dst_r) < 2 and abs(src_c - dst_c) < 2: # Split. Add a new piece, increase the count. action_str = "^Mgrew^~ into" self.board[dst_r][dst_c] = color seat.data.count += 1 else: # Leap. Move the piece, don't increase the count. action_str = "^Cjumped^~ to" self.board[src_r][src_c] = None self.board[dst_r][dst_c] = color # Whichever action occurred, check all cells surrounding the # destination. If they are opponents, transform them. change_count = 0 change_str = "" for r_d in range(-1, 2): for c_d in range(-1, 2): if self.is_valid(dst_r + r_d, dst_c + c_d): occupier = self.board[dst_r + r_d][dst_c + c_d] if occupier and occupier != color and occupier != PIT: # Another player. Uh oh! Flip it and decrement that # player's count. self.board[dst_r + r_d][dst_c + c_d] = color seat.data.count += 1 self.sides[occupier].data.count -= 1 change_count += 1 if change_count: change_str = ", ^!converting %d piece" % change_count if change_count != 1: change_str += "s" # Tell everyone what just happened. self.channel.broadcast_cc(self.prefix + "From ^c%s^~, %s %s ^C%s^~%s^~.\n" % (src_str, player, action_str, dst_str, change_str)) self.update_printable_board() return True def toggle_pits(self, player, loc_list): # Undocumented bonus feature: handles multiple locations, but if # any of them are invalid, it'll bail halfway through. Useful for # prepping a particular cool layout with a single cut-and-pasted # string, though. for loc in loc_list: col, row = loc # Bail if out of bounds. if not self.is_valid(row, col): player.tell_cc(self.prefix + "Pit out of bounds.\n") return # Bail if a starting piece is there. thing_there = self.board[row][col] if thing_there and not (thing_there == PIT): player.tell_cc(self.prefix + "Cannot put a pit on a starting piece.\n") return # Since it's a toggle, figure out what we're toggling to. if thing_there: new_thing = None action_str = "^cremoved^~" else: new_thing = PIT action_str = "^Cadded^~" # Tentative place the thing. self.board[row][col] = new_thing # Does it keep red or blue (which, in a 4p game, is equivalent to # all four players) from being able to make a move? If so, it's # invalid. Put the board back the way it was. if not self.color_has_move(RED) or not self.color_has_move(BLUE): player.tell_cc(self.prefix + "Players must have a valid move.\n") self.board[row][col] = thing_there return loc_list = [(row, col)] edge = self.size - 1 # In either mode, we place another pit across the center line, # but not if that's the same location as the one we just placed # (on the center line on odd-sized boards). if (edge - row) != row: self.board[edge - row][col] = new_thing loc_list.append((edge - row, col)) # Handle the 4p down-reflection if necessary. if self.player_mode == 4 and (edge - col) != col: self.board[edge - row][edge - col] = new_thing loc_list.append((edge - row, edge - col)) # Handle the 4p right-reflection if necessary. if self.player_mode == 4 and (edge - col) != col: self.board[row][edge - col] = new_thing loc_list.append((row, edge - col)) # Generate the list of locations. loc_str = ", ".join(["^C%s^~" % self.loc_to_str(x[0], x[1]) for x in loc_list]) # Finally, send the string detailing what just happened. self.channel.broadcast_cc(self.prefix + "^Y%s^~ has %s a pit at: %s\n" % (player, action_str, loc_str)) self.update_printable_board() def set_size(self, player, size_bits): if not size_bits.isdigit(): player.tell_cc(self.prefix + "Invalid size command.\n") return size = int(size_bits) if size < MIN_SIZE or size > MAX_SIZE: player.tell_cc(self.prefix + "Size must be between %d and %d inclusive.\n" % (MIN_SIZE, MAX_SIZE)) return # Valid! self.size = size self.channel.broadcast_cc(self.prefix + "^R%s^~ has set the board size to ^C%d^~.\n" % (player, size)) self.init_board() self.update_printable_board() def set_player_mode(self, player, mode_bits): if not mode_bits.isdigit(): player.tell_cc(self.prefix + "Invalid player mode command.\n") return mode = int(mode_bits) if mode != 2 and mode != 4: player.tell_cc(self.prefix + "This game only supports two or four players.\n") return elif mode == self.player_mode: player.tell_cc(self.prefix + "This table is already in that mode.\n") return else: self.change_player_mode(mode) self.channel.broadcast_cc(self.prefix + "^Y%s^~ has changed the game to ^C%d-player^~ mode.\n" % (player, mode)) def resign(self, player): seat = self.get_seat_of_player(player) if not seat: player.tell_cc(self.prefix + "You can't resign; you're not playing!\n") return False if self.turn != seat.data.side: player.tell_cc(self.prefix + "You must wait for your turn to resign.\n") return False if seat.data.resigned: player.tell_cc(self.prefix + "You've already resigned.\n") return False # They've passed the tests and can resign. seat.data.resigned = True self.channel.broadcast_cc(self.prefix + "^R%s^~ is resigning from the game.\n" % player) def tick(self): # If all seats are full and the game is active, autostart. active_seats = [x for x in self.seats if x.player] if (self.state.get() == "need_players" and len(active_seats) == self.player_mode and self.active): self.state.set("playing") send_str = "^RRed^~: %s; ^BBlue^~: %s" % (self.seats[0].player_name, self.seats[1].player_name) if self.player_mode == 4: send_str += "; ^GGreen^~: %s; ^YYellow^~: %s" % (self.seats[2].player_name, self.seats[3].player_name) self.channel.broadcast_cc(self.prefix + send_str + "\n") self.turn = RED self.send_board() 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.split() primary = command_bits[0].lower() if state == "setup": if primary in ("size", "sz",): if len(command_bits) == 2: self.set_size(player, command_bits[1]) else: player.tell_cc(self.prefix + "Invalid size command.\n") handled = True elif primary in ("players", "player", "pl",): if len(command_bits) == 2: self.set_player_mode(player, command_bits[1]) else: player.tell_cc(self.prefix + "Invalid player mode command.\n") handled = True elif primary in ("pit", "hole",): loc_list = demangle_move(command_bits[1:]) if loc_list: self.toggle_pits(player, loc_list) else: player.tell_cc(self.prefix + "Invalid pit command.\n") handled = True elif primary in ("ready", "done", "r", "d",): self.channel.broadcast_cc(self.prefix + "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.channel.broadcast_cc(self.prefix + "^R%s^~ has switched the game to setup mode.\n" % player) handled = True elif state == "playing": made_move = False if primary in ("move", "play", "mv", "pl",): move_bits = demangle_move(command_bits[1:]) if move_bits and len(move_bits) == 2: made_move = self.move(player, move_bits[0], move_bits[1]) else: player.tell_cc(self.prefix + "Invalid move command.\n") handled = True elif primary in ("resign",): self.resign(player) made_move = True handled = True if made_move: # Did someone win? winner = self.find_winner() if winner: self.resolve(winner) self.finish() else: # Okay, well, let's see whose turn it is. If it comes # back around to us, the game is over anyway. curr_turn = self.turn done = False while not done: if self.turn == RED: self.turn = BLUE elif self.turn == BLUE: # The only tough one; switch depending on mode. if self.player_mode == 2: self.turn = RED else: self.turn = GREEN elif self.turn == GREEN: self.turn = YELLOW elif self.turn == YELLOW: self.turn = RED # Now see if this player even has a move. if self.color_has_move(self.turn): done = True elif self.turn == curr_turn: # If we've wrapped back around to the current # turn, no one had a move. Bail as well. done = True # Check to see if we're back at the mover. if curr_turn == self.turn: # No one had a valid move. Game's over. self.no_move_resolve() self.finish() else: # Otherwise it's some other player's turn; game on. self.send_board() if not handled: player.tell_cc(self.prefix + "Invalid command.\n") def find_winner(self): # Get the list of players that haven't resigned and have at least one # piece left on the board. If that list is only one long, we have a # winner. Otherwise, the game continues. live_players = [x for x in self.seats if ((not x.data.resigned) and x.data.count)] if len(live_players) == 1: return live_players[0].player_name else: return None def resolve(self, winner): self.send_board() self.channel.broadcast_cc(self.prefix + "^C%s^~ wins!\n" % winner) def no_move_resolve(self): self.send_board() # We look at the number of pieces each player has. Highest wins. high_count = -1 high_list = None for seat in self.seats: if seat.data.count > high_count: high_count = seat.data.count high_list = ["^C%s^~" % seat.player_name] elif seat.data.count == high_count: # Potential tie. high_list.append("^C%s^~" % seat.player_name) # If a single player has the highest count, they win; otherwise, tie. if len(high_list) == 1: self.channel.broadcast_cc(self.prefix + "%s wins with ^Y%d^~ pieces!\n" % (high_list[0], high_count)) else: self.channel.broadcast_cc(self.prefix + "These players ^Rtied^~ for first with ^Y%d^~ pieces: %s\n" % (", ".join(high_list))) def show_help(self, player): super(Ataxx, self).show_help(player) player.tell_cc("\nATAXX SETUP PHASE:\n\n") player.tell_cc(" ^!setup^., ^!config^., ^!conf^. Enter setup phase.\n") player.tell_cc(" ^!size^. <size>, ^!sz^. Set board to <size>.\n") player.tell_cc(" ^!players^. 2|4, ^!pl^. Set number of players.\n") player.tell_cc(" ^!pit^. <ln> Add or remove pit at <ln>.\n") player.tell_cc(" ^!ready^., ^!done^., ^!r^., ^!d^. End setup phase.\n") player.tell_cc("\nATAXX PLAY:\n\n") player.tell_cc(" ^!move^. <ln> <ln2>, ^!mv^. Move from <ln> to <ln2> (letter number).\n") player.tell_cc(" ^!resign^. Resign.\n")
class Set(SeatedGame): """A Set game table implementation. Invented in 1974 by Marsha Jean Falco. """ def __init__(self, server, table_name): super(Set, self).__init__(server, table_name) self.game_display_name = "Set" self.game_name = "set" self.seats = [] self.min_players = 1 self.max_players = 32767 # We don't even use this. self.state = State("need_players") self.prefix = "(^RSet^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Set-specific stuff. self.max_cards_on_table = DEFAULT_MAX_CARDS self.deal_delay = DEFAULT_DEAL_DELAY self.layout = None self.printable_layout = None self.deck = None self.last_play_time = None self.max_card_count = 81 self.has_borders = True def build_deck(self): # Generate the deck... self.deck = [] for count in (ONE, TWO, THREE): fill_list = (SMOOTH, ) if self.has_borders: fill_list = (SMOOTH, WAVY, CHUNKY) for fill in fill_list: for color in (MAGENTA, RED, GREEN): for shape in (BLOB, LOZENGE, SQUIGGLE): self.deck.append((count, fill, color, shape)) # ...and shuffle it. random.shuffle(self.deck) # Trim it to at most the max count. self.deck = self.deck[:self.max_card_count] def build_layout(self): # Put the first twelve cards on the table. self.layout = self.deck[:12] self.deck = self.deck[12:] def update_layout(self): # If the size of the layout is 12 or smaller, this is easy; # we just draw cards left (if any) to fill gaps in the board. # If it's larger than 12, it's easy too; we rebuild the # layout without any gaps (and then, juuust in case, add # blank cards if it somehow got smaller than 12.) layout_len = len(self.layout) if len(self.layout) <= 12: for i in range(layout_len): if not self.layout[i] and self.deck: self.layout[i] = self.deck[0] self.deck = self.deck[1:] else: new_layout = [x for x in self.layout if x] while len(new_layout) < 12: new_layout.append(None) self.layout = new_layout def get_card_art_bits(self, card, line_number): # .----. 1 /~~~~\ |=||=| # |2or3| 2 { } = = # |1or3| 3 { } | | # |2or3| 4 { } = = # `----' 5 \~~~~/ |=||=| # Just in case... if line_number < 1 or line_number > 5: return # At the end of the game, we will sometimes print blank # spaces where cards should go. Handle that. if not card: return " " count, fill, color, shape = card # If a line has a piece of art, it looks like this... art_bit = "%s%s^~" % (color.code, shape.art) # ...otherwise this. blank_bit = " " if line_number == 1: return fill.edge_art[0] elif line_number == 2 or line_number == 4: center = blank_bit if count == TWO or count == THREE: center = art_bit return fill.edge_art[1] % center elif line_number == 3: center = blank_bit if count == ONE or count == THREE: center = art_bit return fill.edge_art[2] % center elif line_number == 5: return fill.edge_art[3] # Dunno how we got here... return "ERROR" def update_printable_layout(self): self.printable_layout = [] if not self.layout: self.printable_layout.append( "The layout is currently ^cempty^~.\n") return # If the layout doesn't have a number of card spaces divisible # by 3, something is horribly wrong, and we should bail. if len(self.layout) % 3 != 0: self.printable_layout.append( "Something is ^Rhorribly wrong^~ with the layout. Alert an admin.\n" ) return # Okay, we have a usable layout. Generate it! cards_per_row = len(self.layout) / 3 self.printable_layout.append("=======" * cards_per_row + "=\n") for row in range(3): if row == 0: row_char = "A" elif row == 1: row_char = "B" else: row_char = "C" for card_line in range(1, 6): this_line = "" for col in range(cards_per_row): this_line += (" %s" % self.get_card_art_bits( self.layout[col * 3 + row], card_line)) self.printable_layout.append(this_line + "\n") # Now we print the codes for each card under the cards. this_line = "" for col in range(1, cards_per_row + 1): this_line += (" %s%s " % (row_char, col)) self.printable_layout.append(this_line + "\n\n") def show(self, player): if not self.printable_layout: self.update_printable_layout() for line in self.printable_layout: player.tell_cc(line) def send_layout(self): for listener in self.channel.listeners: self.show(listener) def join(self, player, join_bits): # We have to override join for two reasons: one, we allow players # to join during the game (!), and two, we have to create seats # for players on the fly, as we have no idea how many there might # end up being. state = self.state.get() if state == "need_players" or state == "playing": if len(join_bits) != 0: player.tell_cc(self.prefix + "Cannot request a specific seat in Set.\n") elif self.get_seat_of_player(player): player.tell_cc(self.prefix + "You're already playing!\n") else: seat = Seat("%s" % str(len(self.seats) + 1)) seat.data.score = 0 self.seats.append(seat) seat.sit(player) player.tell_cc(self.prefix + "You are now sitting in seat %s.\n" % seat) if not self.channel.is_connected(player): self.channel.connect(player) self.channel.broadcast_cc( self.prefix + "^Y%s^~ is now playing in seat ^C%s^~.\n" % (player, seat)) self.num_players += 1 else: player.tell_cc(self.prefix + "Not looking for players.\n") return True def set_max_columns(self, player, column_str): if not column_str.isdigit(): player.tell_cc(self.prefix + "Must provide numbers.\n") return column_val = int(column_str) if column_val < 7 or column_val > 9: player.tell_cc(self.prefix + "Must provide a value between 7 and 9 inclusive.\n") return self.max_cards_on_table = column_val * 3 self.channel.broadcast_cc( self.prefix + "^Y%s^~ set the maximum columns to ^C%s^~.\n" % (player, str(column_val))) def set_delay(self, player, delay_str): if not delay_str.isdigit(): player.tell_cc(self.prefix + "Must provide numbers.\n") return delay_val = int(delay_str) if delay_val < 5 or delay_val > 300: player.tell_cc( self.prefix + "Must provide a value between 5 and 300 inclusive.\n") return self.deal_delay = delay_val self.channel.broadcast_cc( self.prefix + "^Y%s^~ set the deal delay to ^C%s^~ seconds.\n" % (player, str(delay_val))) def set_max_count(self, player, count_str): if not count_str.isdigit(): player.tell_cc(self.prefix + "Must provide numbers.\n") return count_val = int(count_str) if count_val < 21 or count_val > 81 or count_val % 3 != 0: player.tell_cc( self.prefix + "Must provide a value between 21 and 81 inclusive, divisible by 3.\n" ) return self.max_card_count = count_val self.channel.broadcast_cc( self.prefix + "^Y%s^~ set the maximum card count to ^C%s^~ cards.\n" % (player, str(count_val))) def set_border(self, player, border_str): border_bool = booleanize(border_str) if border_bool: if border_bool > 0: self.has_borders = True display_str = "^Con^~" else: self.has_borders = False display_str = "^coff^~" self.channel.broadcast_cc(self.prefix + "^R%s^~ has turned borders %s.\n" % (player, display_str)) else: player.tell_cc(self.prefix + "Not a valid boolean!\n") def handle(self, player, command_str): # Handle common commands first. handled = self.handle_common_commands(player, command_str) if not handled: state = self.state.get() command_bits = command_str.split() primary = command_bits[0].lower() if primary in ("score", "scores"): self.show_scores(player) handled = True elif state == "need_players": if primary in ("column", "columns"): if len(command_bits) == 2: self.set_max_columns(player, command_bits[1]) else: player.tell_cc(self.prefix + "Invalid columns command.\n") handled = True elif primary in ("delay", ): if len(command_bits) == 2: self.set_delay(player, command_bits[1]) else: player.tell_cc(self.prefix + "Invalid delay command.\n") handled = True elif primary in ("cards", "count"): if len(command_bits) == 2: self.set_max_count(player, command_bits[1]) else: player.tell_cc(self.prefix + "Invalid cards command.\n") handled = True elif primary in ("borders", "border"): if len(command_bits) == 2: self.set_border(player, command_bits[1]) else: player.tell_cc(self.prefix + "Invalid border command.\n") handled = True elif primary in ("start", ): if not len(self.seats): player.tell_cc(self.prefix + "Need at least one player!\n") else: self.state.set("playing") self.channel.broadcast_cc(self.prefix + "Game on!\n") self.build_deck() self.build_layout() self.update_printable_layout() self.send_layout() self.last_play_time = time.time() handled = True elif state == "playing": # Everything at this point should be a move, which consists # of a list of 3 card choices. As always, do the polite thing # for players who do the play/pl/move/mv/thing. if primary in ('play', 'move', 'pl', 'mv'): play_bits = demangle_move(command_bits[1:]) else: play_bits = demangle_move(command_bits) if play_bits and len(play_bits) == 3: self.declare(player, play_bits) handled = True if not handled: player.tell_cc(self.prefix + "Invalid command.\n") def tick(self): # If the game hasn't started or is finished, don't bother. if not self.last_play_time or self.state.get() == "finished": return # Also don't bother if the maximum number of cards are already # on the table. if len(self.layout) >= self.max_cards_on_table: return # Also don't bother if the deck is empty. if not self.deck: return # Okay, so, we're playing. See if too much time has passed. curr_time = time.time() if curr_time - self.last_play_time < self.deal_delay: return # Yup. Deal out three new cards. for i in range(3): if self.deck: self.layout.append(self.deck[0]) self.deck = self.deck[1:] self.update_printable_layout() self.send_layout() self.channel.broadcast_cc(self.prefix + "New cards have automatically been dealt.\n") # Update the last play time. self.last_play_time = time.time() def declare(self, player, declare_bits): if not self.get_seat_of_player(player): player.tell_cc( self.prefix + "You're not playing in this game! (But you should join.)\n") return # Check the three cards for validity and convert them to locations # in our linear array. valid = True card_locations = [] cards_per_row = len(self.layout) / 3 for bit in declare_bits: # Letter first. if bit[0] < 0 or bit[0] > 2: valid = False else: addend = bit[0] # And the number. multer = bit[1] if multer < 0 or multer >= cards_per_row: valid = False else: # Phew. Passed all the tests. card_locations.append(multer * 3 + addend) if not valid: player.tell_cc(self.prefix + "You declared an invalid card.\n") return elif ((card_locations[0] == card_locations[1]) or (card_locations[1] == card_locations[2]) or (card_locations[0] == card_locations[2])): player.tell_cc(self.prefix + "You can't declare duplicate cards.\n") return # Okay, so, we potentially have valid cards... cards = [self.layout[x] for x in card_locations] # Bail if any of these are empty locations. if not cards[0] or not cards[1] or not cards[2]: player.tell_cc(self.prefix + "You can't pick empty spaces.\n") return # All right. Three valid, actual cards. Now let's see if they're # actually a set! if self.is_a_set(cards): seat = self.get_seat_of_player(player) seat.data.score += 1 # zomg. Is an actual set! Notify the press. Update the layout # and send it out. for i in card_locations: self.layout[i] = None self.update_layout() self.update_printable_layout() self.send_layout() self.channel.broadcast_cc(self.prefix + "^Y%s^~ found a set! (%s)\n" % (player, self.make_set_str(cards))) # Determine if the game is over. If so, we're done! if self.no_more_sets(): self.resolve() self.finish() # Lastly, mark this as the time of the last valid play. self.last_play_time = time.time() else: player.tell_cc(self.prefix + self.make_set_str(cards) + " is not a set!\n") def third_card(self, one, two): # For any two cards, the third card to make it a set can be # determined easily. For each element, if the two cards are # the same, the third must have the same one as well; if # different, use BITFIELDS to find the missing one. Since # there are 3 possible values, subtract 7 from the summation # of the two's bit values to get the third one. three = [] for k in range(4): if one[k] == two[k]: three.append(one[k]) else: three.append(BITFIELDS[k][7 - (BITFIELDS[k][one[k]] + BITFIELDS[k][two[k]])]) return tuple(three) def is_a_set(self, cards): return self.third_card(cards[0], cards[1]) == cards[2] def make_set_str(self, cards): card_str_list = [] for card in cards: card_str = card[2].code + " ".join([x.display for x in card]) if card[0] != ONE: card_str += "s" card_str_list.append(card_str + "^~") return ", ".join(card_str_list) def no_more_sets(self): # First, bail if the deck still has any cards whatsoever, as we can't # possibly know that there aren't any sets left until the deck is # depleted. if self.deck: return False # Okay, now, get a list of cards on the layout. cards_left = [x for x in self.layout if x] count_left = len(cards_left) # If there are more than 20 cards on the table, we know for a fact # that there has to be a set left. if count_left > 20: return False # Take every unique pair of cards on the layout and determine what # the third card would be that makes them a set. If that card is # still on the table, we have a valid set left. for i in range(count_left): for j in range(i + 1, count_left): if self.third_card(cards_left[i], cards_left[j]) in cards_left: return False # No sets found. return True def resolve(self): winner_dict = {} for seat in self.seats: score = seat.data.score if score in winner_dict: winner_dict[score].append(seat.player_name) else: winner_dict[score] = [seat.player_name] winner_score_list = sorted(winner_dict.keys(), reverse=True) winner_score = winner_score_list[0] self.send_scores() if len(winner_dict[winner_score]) == 1: self.channel.broadcast_cc(self.prefix + "^Y%s^~ is the winner!\n" % (winner_dict[winner_score][0])) else: self.channel.broadcast_cc( self.prefix + "These players tied for first: ^Y%s^~\n" % (", ".join(winner_dict[winner_score]))) def show_scores(self, player): player.tell_cc("\nSCORES:\n\n") state = "yellow" for seat in self.seats: player_str = seat.player_name if state == "yellow": name_color_code = "^Y" score_color_code = "^C" state = "magenta" elif state == "magenta": name_color_code = "^M" score_color_code = "^G" state = "yellow" tell_string = " ^R%s^~: %s%s^~, %s%s^~" % ( seat, name_color_code, player_str, score_color_code, get_plural_str(seat.data.score, "point")) player.tell_cc(tell_string + "\n") player.tell_cc("\n") def send_scores(self): for player in self.channel.listeners: self.show_scores(player) def show_help(self, player): super(Set, self).show_help(player) player.tell_cc("\nSET SETUP PHASE:\n\n") player.tell_cc( " ^!columns^. <num> Set the maximum columns to <num> (7-9).\n" ) player.tell_cc( " ^!delay^. <sec> Set the autodeal delay to <sec> secs.\n" ) player.tell_cc( " ^!cards^. <num> Set the maximum card count to <num>.\n" ) player.tell_cc( " ^!borders^. on|off Set the borders on or off.\n" ) player.tell_cc( " ^!start^. Start the game.\n") player.tell_cc("\nSET PLAY:\n\n") player.tell_cc( " ^!l1^., ^!l2^., ^!l3^. Declare <l1>, <l2>, <l3> a set.\n" ) player.tell_cc( " ^!scores^. See the current scores.\n")
class RockPaperScissors(SeatedGame): """A Rock-Paper-Scissors game table implementation. """ def __init__(self, server, table_name): super(RockPaperScissors, self).__init__(server, table_name) self.game_display_name = "Rock-Paper-Scissors" self.game_name = "rps" self.seats = [ Seat("Left"), Seat("Right"), ] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.plays = [None, None] self.prefix = "(^RRPS^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # RPS requires both seats, so may as well mark them active. self.seats[0].active = True self.seats[1].active = True def handle(self, player, command_str): # Handle common commands. handled = self.handle_common_commands(player, command_str) state = self.state.get() if state == "need_moves": command_bits = command_str.split() primary = command_bits[0].lower() # If this player is used to prefacing plays with 'move'/'play', # let's be polite and just chomp that away. Also allow 'throw' # and 'th', even though they're undocumented, because they seem # like an obvious sort of command to try. (Read that as: I kept # typing it.) if primary in ('move', 'play', 'throw', 'mv', 'pl', 'th') and len(command_bits) > 1: primary = command_bits[1].lower() if primary in ('r', 'p', 's', 'rock', 'paper', 'scissors'): self.move(player, primary) handled = True if self.plays[0] and self.plays[1] and self.active: # Got the moves! self.resolve() self.finish() if not handled: player.tell_cc(self.prefix + "Invalid command.\n") def tick(self): # If we were looking for players, check to see if both # seats are full and the game is active. If so, autostart. if (self.state.get() == "need_players" and self.seats[0].player and self.seats[1].player and self.active): self.state.set("need_moves") self.channel.broadcast_cc(self.prefix + "Left: ^Y%s^~; Right: ^Y%s^~\n" % (self.seats[0].player, self.seats[1].player)) self.channel.broadcast_cc(self.prefix + "Players, make your moves!\n") def show(self, player): state = self.state.get() if state == "need_players": player.tell_cc(self.prefix + "Everyone is hovering around the table, waiting for players.\n") elif state == "need_moves": for loc, color in ((0, "^Y"), (1, "^M")): if self.seats[loc].player: name = repr(self.seats[loc].player) if self.plays[loc]: player.tell_cc(self.prefix + color + name + "^~'s hand is trembling with anticipation.\n") else: player.tell_cc(self.prefix + color + name + "^~ seems to be deep in thought.\n") else: player.tell_cc(self.prefix + "^C%s^~ is strangely empty.\n" % self.seats[loc]) else: player.tell_cc(self.prefix + "Nothing to see here. Move along.\n") def show_help(self, player): super(RockPaperScissors, self).show_help(player) player.tell_cc("\nROCK-PAPER-SCISSORS:\n\n") player.tell_cc(" ^!rock^., ^!r^. Throw rock.\n") player.tell_cc(" ^!paper^., ^!p^. Throw paper.\n") player.tell_cc(" ^!scissors^., ^!s^. Throw scissors.\n") def move(self, player, play): seat = self.get_seat_of_player(player) if not seat: player.tell_cc(self.prefix + "You're not playing in this game!\n") return if play in ('r', 'rock'): this_move = "rock" elif play in ('p', 'paper'): this_move = "paper" elif play in ('s', 'scissors'): this_move = "scissors" else: player.tell_cc(self.prefix + "Invalid play.\n") return self.channel.broadcast_cc(self.prefix + "%s's hand twitches.\n" % player) if seat == self.seats[0]: self.plays[0] = this_move else: self.plays[1] = this_move def resolve(self): one = self.plays[0] two = self.plays[1] one_name = "^Y" + repr(self.seats[0].player) + "^~" two_name = "^M" + repr(self.seats[1].player) + "^~" self.channel.broadcast_cc(self.prefix + "Jan... ken... pon... Throwdown time!\n") self.channel.broadcast_cc(self.prefix + "%s throws ^!%s^.; %s throws ^!%s^.!\n" % (one_name, one, two_name, two)) if one == two: msg = "It's a tie!\n" elif ((one == "rock" and two == "paper") or (one == "paper" and two == "scissors") or (one == "scissors" and two == "rock")): msg = two_name + " wins!\n" else: msg = one_name + " wins!\n" self.channel.broadcast_cc(msg) def remove_player(self, player): # Not only do we want to do the standard things, but if this person # really is a player, we want to invalidate their throw. That way # you're not stuck with another player's throw mid-game. if self.seats[0].player == player: self.plays[0] = None elif self.seats[1].player == player: self.plays[1] = None super(RockPaperScissors, self).remove_player(player)
class Metamorphosis(SeatedGame): """A Metamorphosis game table implementation. Invented in 2009 by Gregory Keith Van Patten. Play seems to show that ko fight mode is definitely superior to the alternative, so we set it as default. """ def __init__(self, server, table_name): super(Metamorphosis, self).__init__(server, table_name) self.game_display_name = "Metamorphosis" self.game_name = "metamorphosis" self.seats = [Seat("Black"), Seat("White")] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.prefix = "(^RMetamorphosis^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Metamorphosis-specific stuff. self.board = None self.printable_board = None self.size = 12 self.ko_fight = True self.group_count = None self.turn = None self.turn_number = 0 self.seats[0].data.side = BLACK self.seats[0].data.last_was_ko = False self.seats[1].data.side = WHITE self.seats[1].data.last_was_ko = False self.last_r = None self.last_c = None self.resigner = None self.adjacency_map = None self.found_winner = False self.init_board() def init_board(self): self.board = [] # Generate a new empty board. Boards alternate the starting checkered # layout, which we can pregen. white_first_row = [] black_first_row = [] for c in range(self.size): if c % 2: white_first_row.append(BLACK) black_first_row.append(WHITE) else: white_first_row.append(WHITE) black_first_row.append(BLACK) # Then we just add the appropriate rows, two at a time. for r in range(self.size / 2): self.board.append(white_first_row[:]) self.board.append(black_first_row[:]) # Count the number of groups on the board. Should be size^2. self.group_count = self.get_group_count() def update_printable_board(self): self.printable_board = [] col_str = " " + "".join([" " + COLS[i] for i in range(self.size)]) self.printable_board.append(col_str + "\n") self.printable_board.append(" ^m.=" + "".join(["=="] * self.size) + ".^~\n") for r in range(self.size): this_str = "%2d ^m|^~ " % (r + 1) for c in range(self.size): if r == self.last_r and c == self.last_c: this_str += "^5" loc = self.board[r][c] if loc == WHITE: this_str += "^Wo^~ " elif loc == BLACK: this_str += "^Kx^~ " else: this_str += "^M.^~ " this_str += "^m|^~ %d" % (r + 1) self.printable_board.append(this_str + "\n") self.printable_board.append(" ^m`=" + "".join(["=="] * self.size) + "'^~\n") self.printable_board.append(col_str + "\n") def show(self, player): if not self.printable_board: self.update_printable_board() for line in self.printable_board: player.tell_cc(line) player.tell_cc(self.get_turn_str() + "\n") def send_board(self): for player in self.channel.listeners: self.show(player) def get_turn_str(self): if not self.turn: return ("The game has not yet started.\n") if self.turn == BLACK: player = self.seats[0].player_name color_msg = "^KBlack/Vertical^~" else: player = self.seats[1].player_name color_msg = "^WWhite/Horizontal^~" return ("It is ^Y%s^~'s turn (%s)." % (player, color_msg)) def is_valid(self, row, col): if row < 0 or row >= self.size or col < 0 or col >= self.size: return False return True def get_group_count(self): # Counting groups can be done in a single pass, even if the groups # are complicated. It is done as follows: # - Check the color in a location and the locations above and to the # left. # - If it's the same color as only one of them, assign ourselves the # group ID of that piece; # - If it's the same color as /both/, and they have distinct group IDs, # reassign the higher-numbered group ID to the lower group ID, as this # piece joins those two groups; # - If it's the same color as neither, add a new group ID. # At the end, we simply count the number of distinct group IDs. # Build a temporary board tracking the IDs of each spot. id_board = [] for r in range(self.size): id_board.append([None] * self.size) id_dict = {} curr_id = 0 for r in range(self.size): for c in range(self.size): self_color = self.board[r][c] above_color = None left_color = None if r - 1 >= 0: above_color = self.board[r - 1][c] if c - 1 >= 0: left_color = self.board[r][c - 1] if self_color == above_color: # Definitely a group above. Check to see if it's the same # to the left too. group_id = id_dict[id_board[r - 1][c]] if self_color == left_color: # Same color. Are they the same group ID? left_id = id_dict[id_board[r][c - 1]] if left_id == group_id: # Yup. Use it. id_board[r][c] = group_id else: # This piece joins two previously-distinct groups. # Update the ID dict to collapse those two groups # into one and use the lower-valued one for this # space. if left_id < group_id: id_dict[group_id] = left_id id_board[r][c] = left_id else: id_dict[left_id] = group_id id_board[r][c] = group_id else: # Different color left, same color above. Use the # above ID. id_board[r][c] = group_id elif self_color == left_color: # Group to the left, but not above. Use the left ID. id_board[r][c] = id_dict[id_board[r][c - 1]] else: # No group in either direction. New group. id_board[r][c] = curr_id id_dict[curr_id] = curr_id curr_id += 1 # Now that we're done with those shenanigans, count the number of unique # groups on the board. unique_set = set() for group_id in id_dict: if id_dict[group_id] not in unique_set: unique_set.add(id_dict[group_id]) return len(unique_set) def flip(self, row, col): curr = self.board[row][col] if curr == BLACK: self.board[row][col] = WHITE else: self.board[row][col] = BLACK def move(self, player, play): seat = self.get_seat_of_player(player) if not seat: player.tell_cc(self.prefix + "You can't move; you're not playing!\n") return False if self.turn != seat.data.side: player.tell_cc(self.prefix + "You must wait for your turn to move.\n") return False col, row = play # Make sure they're all in range. if not self.is_valid(row, col): player.tell_cc(self.prefix + "Your move is out of bounds.\n") return False # Does this move increase the number of groups on the board? self.flip(row, col) new_group_count = self.get_group_count() if new_group_count > self.group_count: # Yup. Flip it back and inform the player. self.flip(row, col) player.tell_cc(self.prefix + "That move increases the group count.\n") return False move_is_ko = False ko_str = "" # If we're in ko fight mode, check to see if a ko move is valid. if new_group_count == self.group_count: move_is_ko = True ko_str = ", a ko move" if not self.ko_fight: # Flip it back; we're not in ko fight mode. self.flip(row, col) player.tell_cc( self.prefix + "That is a ko move and does not decrease the group count.\n" ) return False elif seat.data.last_was_ko: # Flip it back; two kos in a row is not allowed. self.flip(row, col) player.tell_cc( self.prefix + "That is a ko move and you made a ko move last turn.\n") return False elif row == self.last_r and col == self.last_c: # Flip it back; this is the same move their opponent just made. self.flip(row, col) player.tell_cc( self.prefix + "You cannot repeat your opponent's last move.\n") return False # This is a valid move. Apply, announce. play_str = "%s%s" % (COLS[col], row + 1) self.channel.broadcast_cc(self.prefix + "^Y%s^~ flips the piece at ^C%s^~%s.\n" % (seat.player, play_str, ko_str)) self.last_r = row self.last_c = col self.turn_number += 1 self.group_count = new_group_count # If it was a ko move, mark the player as having made one, so they # can't make another the next turn. Otherwise clear that bit. if move_is_ko: seat.data.last_was_ko = True else: seat.data.last_was_ko = False return True 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.channel.broadcast_cc( self.prefix + "^KBlack/Vertical^~: ^R%s^~; ^WWhite/Horizontal^~: ^Y%s^~\n" % (self.seats[0].player, self.seats[1].player)) self.turn = BLACK self.turn_number = 1 self.send_board() def set_size(self, player, size_bits): if not size_bits.isdigit(): player.tell_cc(self.prefix + "Invalid size command.\n") return size = int(size_bits) if size < MIN_SIZE or size > MAX_SIZE: player.tell_cc(self.prefix + "Size must be between %d and %d inclusive.\n" % (MIN_SIZE, MAX_SIZE)) return if size % 2: player.tell_cc(self.prefix + "Size must be even.\n") return # Valid! self.size = size self.channel.broadcast_cc( self.prefix + "^R%s^~ has set the board size to ^C%d^~.\n" % (player, size)) self.init_board() self.update_printable_board() def set_ko_fight(self, player, ko_str): ko_bool = booleanize(ko_str) if ko_bool: if ko_bool > 0: self.ko_fight = True display_str = "^Con^~" else: self.ko_fight = False display_str = "^coff^~" self.channel.broadcast_cc( self.prefix + "^R%s^~ has turned ^Gko fight^~ mode %s.\n" % (player, display_str)) def resign(self, player): seat = self.get_seat_of_player(player) if not seat: player.tell_cc(self.prefix + "You can't resign; you're not playing!\n") return False if self.turn != seat.data.side: player.tell_cc(self.prefix + "You must wait for your turn to resign.\n") return False self.resigner = seat.data.side self.channel.broadcast_cc(self.prefix + "^R%s^~ is resigning from the game.\n" % player) return True def swap(self, player): # Like Hex, a swap in Metamorphosis requires a translation to make it the # equivalent move for the other player. self.flip(self.last_r, self.last_c) self.flip(self.last_c, self.last_r) self.last_c, self.last_r = self.last_r, self.last_c self.channel.broadcast_cc( "^Y%s^~ has swapped ^KBlack^~'s first move.\n" % (player)) self.turn_number += 1 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.split() primary = command_bits[0] if state == "setup": if primary in ( "size", "sz", ): if len(command_bits) == 2: self.set_size(player, command_bits[1]) else: player.tell_cc(self.prefix + "Invalid size command.\n") handled = True elif primary in ("ko", ): if len(command_bits) == 2: self.set_ko_fight(player, command_bits[1]) else: player.tell_cc(self.prefix + "Invalid ko command.\n") handled = True if primary in ( "done", "ready", "d", "r", ): self.channel.broadcast_cc( self.prefix + "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.channel.broadcast_cc( self.prefix + "^R%s^~ has switched the game to setup mode.\n" % player) handled = True elif state == "playing": made_move = False if primary in ( "move", "play", "mv", "pl", ): invalid = False move_bits = demangle_move(command_bits[1:]) if move_bits and len(move_bits) == 1: made_move = self.move(player, move_bits[0]) else: invalid = True if invalid: player.tell_cc(self.prefix + "Invalid move command.\n") handled = True elif primary in ("swap", ): if self.seats[1].player == player and self.turn_number == 2: self.swap(player) made_move = True else: player.tell_cc(self.prefix + "Invalid swap command.\n") handled = True elif primary in ("resign", ): if self.resign(player): made_move = True handled = True if made_move: # Okay, something happened on the board. Update. self.update_printable_board() # Did someone win? winner = self.find_winner() if winner: self.resolve(winner) self.finish() else: # Nope. Switch turns... if self.turn == BLACK: self.turn = WHITE else: self.turn = BLACK # ...show everyone the board, and keep on. self.send_board() if not handled: player.tell_cc(self.prefix + "Invalid command.\n") def find_winner(self): # If someone resigned, this is the easiest thing ever. if self.resigner == WHITE: return self.seats[0].player_name elif self.resigner == BLACK: return self.seats[1].player_name # This is like most connection games; we check recursively from the # top and left edges to see whether a player has won. self.found_winner = False self.adjacency_map = [] for i in range(self.size): self.adjacency_map.append([None] * self.size) for i in range(self.size): if self.board[i][0] == WHITE: self.recurse_adjacency(WHITE, i, 0) if self.board[0][i] == BLACK: self.recurse_adjacency(BLACK, 0, i) # ...except that it has to be at the end of the OTHER player's turn! if self.found_winner == BLACK and self.turn == WHITE: return self.seats[0].player_name elif self.found_winner == WHITE and self.turn == BLACK: return self.seats[1].player_name # No winner yet. return None def recurse_adjacency(self, color, row, col): # Bail if we found a winner already. if self.found_winner: return # Bail if we're off the board. if not self.is_valid(row, col): return # Bail if we've been here. if self.adjacency_map[row][col]: return # Bail if this is the wrong color. if self.board[row][col] != color: return # Okay. Occupied and it's this player's. Mark. self.adjacency_map[row][col] = True # Have we hit the winning side for this player? if ((color == WHITE and col == self.size - 1) or (color == BLACK and row == self.size - 1)): # Success! self.found_winner = color return # Not a win yet. Recurse over adjacencies. for r_delta, c_delta in CONNECTION_DELTAS: self.recurse_adjacency(color, row + r_delta, col + c_delta) def resolve(self, winner): self.send_board() self.channel.broadcast_cc(self.prefix + "^C%s^~ wins!\n" % winner) def show_help(self, player): super(Metamorphosis, self).show_help(player) player.tell_cc("\nMETAMORPHOSIS SETUP PHASE:\n\n") player.tell_cc( " ^!setup^., ^!config^., ^!conf^. Enter setup phase.\n" ) player.tell_cc( " ^!ko^. on|off Enable/disable ko fight mode.\n" ) player.tell_cc( " ^!size^. <size>, ^!sz^. Set board to <size>.\n") player.tell_cc( " ^!ready^., ^!done^., ^!r^., ^!d^. End setup phase.\n" ) player.tell_cc("\nMETAMORPHOSIS PLAY:\n\n") player.tell_cc( " ^!move^. <ln>, ^!play^., ^!mv^., ^!pl^. Make move <ln> (letter number).\n" ) player.tell_cc( " ^!swap^. Swap the first move (only White, only their first).\n" ) player.tell_cc(" ^!resign^. Resign.\n")
class Whist(SeatedGame): """A Whist game table implementation. Whist came about sometime in the 18th century. This implementation does not (currently) score honours, because honours are boring. """ def __init__(self, server, table_name): super(Whist, self).__init__(server, table_name) self.game_display_name = "Whist" self.game_name = "whist" self.seats = [ Seat("North"), Seat("East"), Seat("South"), Seat("West"), ] self.min_players = 4 self.max_players = 4 self.state = State("need_players") self.prefix = "(^RWhist^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Whist-specific guff. self.ns = Struct() self.ns.score = 0 self.ew = Struct() self.ew.score = 0 self.seats[0].data.who = NORTH self.seats[1].data.who = EAST self.seats[2].data.who = SOUTH self.seats[3].data.who = WEST self.goal = 5 self.trick = None self.trump_suit = None self.led_suit = None self.turn = None self.dealer = None self.winner = None self.layout = FourPlayerCardGameLayout() def show_help(self, player): super(Whist, self).show_help(player) player.tell_cc("\nWHIST SETUP PHASE:\n\n") player.tell_cc(" ^!setup^., ^!config^., ^!conf^. Enter setup phase.\n") player.tell_cc(" ^!goal^. <num>, ^!score^. Set the goal score to <num>.\n") player.tell_cc(" ^!ready^., ^!done^., ^!r^., ^!d^. End setup phase.\n") player.tell_cc("\nWHIST PLAY:\n\n") player.tell_cc(" ^!play^. <card>, ^!pl^. Play <card> from your hand.\n") player.tell_cc(" ^!hand^., ^!inv^., ^!i^. Look at the cards in your hand.\n") def display(self, player): player.tell_cc("%s" % self.layout) def get_score_str(self): return " ^RNorth/South^~: %d ^MEast/West^~: %d\n" % (self.ns.score, self.ew.score) def get_color_code(self, seat): if seat == self.seats[0] or seat == self.seats[2]: return "^R" else: return "^M" def get_sp_str(self, seat): return "^G%s^~ (%s%s^~)" % (seat.player_name, self.get_color_code(seat), seat) def get_metadata(self): to_return = "\n\n" if self.turn: to_return += "It is ^Y%s^~'s turn (%s%s^~). Trumps are ^C%s^~.\n" % (self.turn.player_name, self.get_color_code(self.turn), self.turn, self.trump_suit) to_return += "Tricks: ^RNorth/South^~: %d ^MEast/West^~: %d\n" % (self.ns.tricks, self.ew.tricks) to_return += "The goal score for this game is ^C%s^~.\n" % get_plural_str(self.goal, "point") to_return += self.get_score_str() return to_return def show(self, player): self.display(player) player.tell_cc(self.get_metadata()) 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 clear_trick(self): # Set the current trick to an empty hand... self.trick = Hand() self.led_suit = None # ...and set everyone's played card to None. for seat in self.seats: seat.data.card = None # Clear the layout as well. self.layout.clear() def new_deal(self): dealer_name = self.dealer.player_name self.bc_pre("^R%s^~ (%s%s^~) gives the cards a good shuffle...\n" % (dealer_name, self.get_color_code(self.dealer), self.dealer)) deck = new_deck() deck.shuffle() # Deal out all of the cards. We'll flip the last one; that determines # the trump suit for the hand. self.bc_pre("^R%s^~ deals the cards out to all the players.\n" % dealer_name) for seat in self.seats: seat.data.hand = Hand() for i in range(13): for seat in self.seats: seat.data.hand.add(deck.discard()) # Flip the dealer's last card; it determines the trump suit. last_card = self.dealer.data.hand[-1] self.bc_pre("^R%s^~ flips their last card; it is ^C%s^~.\n" % (dealer_name, card_to_str(last_card, LONG))) self.trump_suit = last_card.suit # Sort everyone's hands. for seat in self.seats: seat.data.hand = sorted_hand(seat.data.hand, self.trump_suit) # Show everyone their hands. self.show_hands() # Set the trick counts to zero. self.ns.tricks = 0 self.ew.tricks = 0 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, self.trump_suit) print_str += "\n" self.tell_pre(player, print_str) def show_hands(self): for seat in self.seats: if seat.player: self.show_hand(seat.player) 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 # 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 # Okay, it's a card in their hand. First, let's do the "follow the # led suit" business. action_str = "^Wplays^~" if self.led_suit: this_suit = potential_card.suit if (this_suit != self.led_suit and hand_has_suit(seat.data.hand, self.led_suit)): # You can't play off-suit if you can match the led suit. self.tell_pre(player, "You can't throw off; you have the led suit.\n") return False else: # No led suit; they're the leader. action_str = "^Yleads^~ with" self.led_suit = potential_card.suit # They either matched the led suit, didn't have any of it, or they # are themselves the leader. Nevertheless, their play is valid. seat.data.card = potential_card self.trick.add(seat.data.hand.discard_specific(potential_card)) trump_str = "" if potential_card.suit == self.trump_suit: trump_str = ", a ^Rtrump^~" self.bc_pre("%s %s ^C%s^~%s.\n" % (self.get_sp_str(seat), action_str, card_to_str(potential_card, LONG), trump_str)) self.layout.place(seat.data.who, potential_card) return potential_card def tick(self): # If all seats are full and active, autostart. if (self.state.get() == "need_players" and self.seats[0].player and self.seats[1].player and self.seats[2].player and self.seats[3].player and self.active): self.state.set("playing") self.bc_pre("The game has begun.\n") # Initialize everything by clearing the (non-existent) trick. self.clear_trick() # Make a new deal. self.dealer = self.seats[0] self.new_deal() # Eldest leads to the first trick. self.turn = self.next_seat(self.dealer) self.layout.change_turn(self.turn.data.who) 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.split() primary = command_bits[0].lower() if state == "setup": if primary in ("goal", "score", "sc", "g",): 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": card_played = False if primary in ("hand", "inventory", "inv", "i",): self.show_hand(player) handled = True elif primary in ("play", "move", "pl", "mv",): if len(command_bits) == 2: card_played = self.play(player, command_bits[1]) else: self.tell_pre(player, "Invalid play command.\n") handled = True if card_played: # A card hit the table. We need to do stuff. if len(self.trick) == 4: # Finish the trick up. self.finish_trick() # Is that the last trick of this hand? if self.ns.tricks + self.ew.tricks == 13: # Yup. Finish the hand up. self.finish_hand() # Did someone win the overall game? winner = self.find_winner() if winner: # Yup. Finish. self.resolve(winner) self.finish() else: # Nope. Pass the deal to the next dealer... self.dealer = self.next_seat(self.dealer) # Deal and set up the first player. self.new_deal() self.turn = self.next_seat(self.dealer) self.layout.change_turn(self.turn.data.who) else: # Trick not over. Rotate. self.turn = self.next_seat(self.turn) self.layout.change_turn(self.turn.data.who) if self.turn.player: self.show_hand(self.turn.player) if not handled: self.tell_pre(player, "Invalid command.\n") def finish_trick(self): # Okay, we have a trick with four cards. Which card won? winner = handle_trick(self.trick, self.trump_suit) # This /should/ just return one seat... winning_seat_list = [x for x in self.seats if x.data.card == winner] if len(winning_seat_list) != 1: self.server.log.log(self.log_prefix + "Something went horribly awry; trick ended without a finish.") self.bc_pre("Something went horribly wrong; no one won the trick! Tell the admin.\n") return winning_seat = winning_seat_list[0] # Print information about the winning card. self.bc_pre("%s wins the trick with ^C%s^~.\n" % (self.get_sp_str(winning_seat), card_to_str(winner, LONG))) # Give the trick to the correct partnership. if winning_seat == self.seats[0] or winning_seat == self.seats[2]: self.ns.tricks += 1 else: self.ew.tricks += 1 # Clear the trick. self.clear_trick() # Set the next leader to the player who won. self.turn = winning_seat self.layout.change_turn(self.turn.data.who) if self.turn.player: self.show_hand(self.turn.player) def finish_hand(self): # Which side won more than 6 tricks? if self.ns.tricks > 6: winning_side = "^RNorth/South^~" addend = self.ns.tricks - 6 self.ns.score += addend else: winning_side = "^MEast/West^~" addend = self.ew.tricks - 6 self.ew.score += addend # Let everyone know. self.bc_pre("%s wins the hand and gains ^C%s^~.\n" % (winning_side, get_plural_str(addend, "point"))) self.bc_pre(self.get_score_str()) def find_winner(self): # Easy: has one of the sides reached a winning score? if self.ns.score >= self.goal: return self.ns elif self.ew.score >= self.goal: return self.ew return None def resolve(self, winning_partnership): if self.ns == winning_partnership: name_one = self.seats[0].player_name name_two = self.seats[2].player_name else: name_one = self.seats[1].player_name name_two = self.seats[3].player_name self.bc_pre("^G%s^~ and ^G%s^~ win!\n" % (name_one, name_two))
class Set(SeatedGame): """A Set game table implementation. Invented in 1974 by Marsha Jean Falco. """ def __init__(self, server, table_name): super(Set, self).__init__(server, table_name) self.game_display_name = "Set" self.game_name = "set" self.seats = [] self.min_players = 1 self.max_players = 32767 # We don't even use this. self.state = State("need_players") self.prefix = "(^RSet^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Set-specific stuff. self.max_cards_on_table = DEFAULT_MAX_CARDS self.deal_delay = DEFAULT_DEAL_DELAY self.layout = None self.printable_layout = None self.deck = None self.last_play_time = None self.max_card_count = 81 self.has_borders = True def build_deck(self): # Generate the deck... self.deck = [] for count in (ONE, TWO, THREE): fill_list = (SMOOTH,) if self.has_borders: fill_list = (SMOOTH, WAVY, CHUNKY) for fill in fill_list: for color in (MAGENTA, RED, GREEN): for shape in (BLOB, LOZENGE, SQUIGGLE): self.deck.append((count, fill, color, shape)) # ...and shuffle it. random.shuffle(self.deck) # Trim it to at most the max count. self.deck = self.deck[:self.max_card_count] def build_layout(self): # Put the first twelve cards on the table. self.layout = self.deck[:12] self.deck = self.deck[12:] def update_layout(self): # If the size of the layout is 12 or smaller, this is easy; # we just draw cards left (if any) to fill gaps in the board. # If it's larger than 12, it's easy too; we rebuild the # layout without any gaps (and then, juuust in case, add # blank cards if it somehow got smaller than 12.) layout_len = len(self.layout) if len(self.layout) <= 12: for i in range(layout_len): if not self.layout[i] and self.deck: self.layout[i] = self.deck[0] self.deck = self.deck[1:] else: new_layout = [x for x in self.layout if x] while len(new_layout) < 12: new_layout.append(None) self.layout = new_layout def get_card_art_bits(self, card, line_number): # .----. 1 /~~~~\ |=||=| # |2or3| 2 { } = = # |1or3| 3 { } | | # |2or3| 4 { } = = # `----' 5 \~~~~/ |=||=| # Just in case... if line_number < 1 or line_number > 5: return # At the end of the game, we will sometimes print blank # spaces where cards should go. Handle that. if not card: return " " count, fill, color, shape = card # If a line has a piece of art, it looks like this... art_bit = "%s%s^~" % (color.code, shape.art) # ...otherwise this. blank_bit = " " if line_number == 1: return fill.edge_art[0] elif line_number == 2 or line_number == 4: center = blank_bit if count == TWO or count == THREE: center = art_bit return fill.edge_art[1] % center elif line_number == 3: center = blank_bit if count == ONE or count == THREE: center = art_bit return fill.edge_art[2] % center elif line_number == 5: return fill.edge_art[3] # Dunno how we got here... return "ERROR" def update_printable_layout(self): self.printable_layout = [] if not self.layout: self.printable_layout.append("The layout is currently ^cempty^~.\n") return # If the layout doesn't have a number of card spaces divisible # by 3, something is horribly wrong, and we should bail. if len(self.layout) % 3 != 0: self.printable_layout.append("Something is ^Rhorribly wrong^~ with the layout. Alert an admin.\n") return # Okay, we have a usable layout. Generate it! cards_per_row = len(self.layout) / 3 self.printable_layout.append("=======" * cards_per_row + "=\n") for row in range(3): if row == 0: row_char = "A" elif row == 1: row_char = "B" else: row_char = "C" for card_line in range(1, 6): this_line = "" for col in range(cards_per_row): this_line += (" %s" % self.get_card_art_bits(self.layout[col * 3 + row], card_line)) self.printable_layout.append(this_line + "\n") # Now we print the codes for each card under the cards. this_line = "" for col in range(1, cards_per_row + 1): this_line += (" %s%s " % (row_char, col)) self.printable_layout.append(this_line + "\n\n") def show(self, player): if not self.printable_layout: self.update_printable_layout() for line in self.printable_layout: player.tell_cc(line) def send_layout(self): for listener in self.channel.listeners: self.show(listener) def join(self, player, join_bits): # We have to override join for two reasons: one, we allow players # to join during the game (!), and two, we have to create seats # for players on the fly, as we have no idea how many there might # end up being. state = self.state.get() if state == "need_players" or state == "playing": if len(join_bits) != 0: player.tell_cc(self.prefix + "Cannot request a specific seat in Set.\n") elif self.get_seat_of_player(player): player.tell_cc(self.prefix + "You're already playing!\n") else: seat = Seat("%s" % str(len(self.seats) + 1)) seat.data.score = 0 self.seats.append(seat) seat.sit(player) player.tell_cc(self.prefix + "You are now sitting in seat %s.\n" % seat) if not self.channel.is_connected(player): self.channel.connect(player) self.channel.broadcast_cc(self.prefix + "^Y%s^~ is now playing in seat ^C%s^~.\n" % (player, seat)) self.num_players += 1 else: player.tell_cc(self.prefix + "Not looking for players.\n") return True def set_max_columns(self, player, column_str): if not column_str.isdigit(): player.tell_cc(self.prefix + "Must provide numbers.\n") return column_val = int(column_str) if column_val < 7 or column_val > 9: player.tell_cc(self.prefix + "Must provide a value between 7 and 9 inclusive.\n") return self.max_cards_on_table = column_val * 3 self.channel.broadcast_cc(self.prefix + "^Y%s^~ set the maximum columns to ^C%s^~.\n" % (player, str(column_val))) def set_delay(self, player, delay_str): if not delay_str.isdigit(): player.tell_cc(self.prefix + "Must provide numbers.\n") return delay_val = int(delay_str) if delay_val < 5 or delay_val > 300: player.tell_cc(self.prefix + "Must provide a value between 5 and 300 inclusive.\n") return self.deal_delay = delay_val self.channel.broadcast_cc(self.prefix + "^Y%s^~ set the deal delay to ^C%s^~ seconds.\n" % (player, str(delay_val))) def set_max_count(self, player, count_str): if not count_str.isdigit(): player.tell_cc(self.prefix + "Must provide numbers.\n") return count_val = int(count_str) if count_val < 21 or count_val > 81 or count_val %3 != 0: player.tell_cc(self.prefix + "Must provide a value between 21 and 81 inclusive, divisible by 3.\n") return self.max_card_count = count_val self.channel.broadcast_cc(self.prefix + "^Y%s^~ set the maximum card count to ^C%s^~ cards.\n" % (player, str(count_val))) def set_border(self, player, border_str): border_bool = booleanize(border_str) if border_bool: if border_bool > 0: self.has_borders = True display_str = "^Con^~" else: self.has_borders = False display_str = "^coff^~" self.channel.broadcast_cc(self.prefix + "^R%s^~ has turned borders %s.\n" % (player, display_str)) else: player.tell_cc(self.prefix + "Not a valid boolean!\n") def handle(self, player, command_str): # Handle common commands first. handled = self.handle_common_commands(player, command_str) if not handled: state = self.state.get() command_bits = command_str.split() primary = command_bits[0].lower() if primary in ("score", "scores"): self.show_scores(player) handled = True elif state == "need_players": if primary in ("column", "columns"): if len(command_bits) == 2: self.set_max_columns(player, command_bits[1]) else: player.tell_cc(self.prefix + "Invalid columns command.\n") handled = True elif primary in ("delay",): if len(command_bits) == 2: self.set_delay(player, command_bits[1]) else: player.tell_cc(self.prefix + "Invalid delay command.\n") handled = True elif primary in ("cards", "count"): if len(command_bits) == 2: self.set_max_count(player, command_bits[1]) else: player.tell_cc(self.prefix + "Invalid cards command.\n") handled = True elif primary in ("borders", "border"): if len(command_bits) == 2: self.set_border(player, command_bits[1]) else: player.tell_cc(self.prefix + "Invalid border command.\n") handled = True elif primary in ("start",): if not len(self.seats): player.tell_cc(self.prefix + "Need at least one player!\n") else: self.state.set("playing") self.channel.broadcast_cc(self.prefix + "Game on!\n") self.build_deck() self.build_layout() self.update_printable_layout() self.send_layout() self.last_play_time = time.time() handled = True elif state == "playing": # Everything at this point should be a move, which consists # of a list of 3 card choices. As always, do the polite thing # for players who do the play/pl/move/mv/thing. if primary in ('play', 'move', 'pl', 'mv'): play_bits = demangle_move(command_bits[1:]) else: play_bits = demangle_move(command_bits) if play_bits and len(play_bits) == 3: self.declare(player, play_bits) handled = True if not handled: player.tell_cc(self.prefix + "Invalid command.\n") def tick(self): # If the game hasn't started or is finished, don't bother. if not self.last_play_time or self.state.get() == "finished": return # Also don't bother if the maximum number of cards are already # on the table. if len(self.layout) >= self.max_cards_on_table: return # Also don't bother if the deck is empty. if not self.deck: return # Okay, so, we're playing. See if too much time has passed. curr_time = time.time() if curr_time - self.last_play_time < self.deal_delay: return # Yup. Deal out three new cards. for i in range(3): if self.deck: self.layout.append(self.deck[0]) self.deck = self.deck[1:] self.update_printable_layout() self.send_layout() self.channel.broadcast_cc(self.prefix + "New cards have automatically been dealt.\n") # Update the last play time. self.last_play_time = time.time() def declare(self, player, declare_bits): if not self.get_seat_of_player(player): player.tell_cc(self.prefix + "You're not playing in this game! (But you should join.)\n") return # Check the three cards for validity and convert them to locations # in our linear array. valid = True card_locations = [] cards_per_row = len(self.layout) / 3 for bit in declare_bits: # Letter first. if bit[0] < 0 or bit[0] > 2: valid = False else: addend = bit[0] # And the number. multer = bit[1] if multer < 0 or multer >= cards_per_row: valid = False else: # Phew. Passed all the tests. card_locations.append(multer * 3 + addend) if not valid: player.tell_cc(self.prefix + "You declared an invalid card.\n") return elif ((card_locations[0] == card_locations[1]) or (card_locations[1] == card_locations[2]) or (card_locations[0] == card_locations[2])): player.tell_cc(self.prefix + "You can't declare duplicate cards.\n") return # Okay, so, we potentially have valid cards... cards = [self.layout[x] for x in card_locations] # Bail if any of these are empty locations. if not cards[0] or not cards[1] or not cards[2]: player.tell_cc(self.prefix + "You can't pick empty spaces.\n") return # All right. Three valid, actual cards. Now let's see if they're # actually a set! if self.is_a_set(cards): seat = self.get_seat_of_player(player) seat.data.score += 1 # zomg. Is an actual set! Notify the press. Update the layout # and send it out. for i in card_locations: self.layout[i] = None self.update_layout() self.update_printable_layout() self.send_layout() self.channel.broadcast_cc(self.prefix + "^Y%s^~ found a set! (%s)\n" % (player, self.make_set_str(cards))) # Determine if the game is over. If so, we're done! if self.no_more_sets(): self.resolve() self.finish() # Lastly, mark this as the time of the last valid play. self.last_play_time = time.time() else: player.tell_cc(self.prefix + self.make_set_str(cards) + " is not a set!\n") def third_card(self, one, two): # For any two cards, the third card to make it a set can be # determined easily. For each element, if the two cards are # the same, the third must have the same one as well; if # different, use BITFIELDS to find the missing one. Since # there are 3 possible values, subtract 7 from the summation # of the two's bit values to get the third one. three = [] for k in range(4): if one[k] == two[k]: three.append(one[k]) else: three.append(BITFIELDS[k][7 - (BITFIELDS[k][one[k]] + BITFIELDS[k][two[k]])]) return tuple(three) def is_a_set(self, cards): return self.third_card(cards[0], cards[1]) == cards[2] def make_set_str(self, cards): card_str_list = [] for card in cards: card_str = card[2].code + " ".join([x.display for x in card]) if card[0] != ONE: card_str += "s" card_str_list.append(card_str + "^~") return ", ".join(card_str_list) def no_more_sets(self): # First, bail if the deck still has any cards whatsoever, as we can't # possibly know that there aren't any sets left until the deck is # depleted. if self.deck: return False # Okay, now, get a list of cards on the layout. cards_left = [x for x in self.layout if x] count_left = len(cards_left) # If there are more than 20 cards on the table, we know for a fact # that there has to be a set left. if count_left > 20: return False # Take every unique pair of cards on the layout and determine what # the third card would be that makes them a set. If that card is # still on the table, we have a valid set left. for i in range(count_left): for j in range(i + 1, count_left): if self.third_card(cards_left[i], cards_left[j]) in cards_left: return False # No sets found. return True def resolve(self): winner_dict = {} for seat in self.seats: score = seat.data.score if score in winner_dict: winner_dict[score].append(seat.player_name) else: winner_dict[score] = [seat.player_name] winner_score_list = sorted(winner_dict.keys(), reverse=True) winner_score = winner_score_list[0] self.send_scores() if len(winner_dict[winner_score]) == 1: self.channel.broadcast_cc(self.prefix + "^Y%s^~ is the winner!\n" % (winner_dict[winner_score][0])) else: self.channel.broadcast_cc(self.prefix + "These players tied for first: ^Y%s^~\n" % (", ".join(winner_dict[winner_score]))) def show_scores(self, player): player.tell_cc("\nSCORES:\n\n") state = "yellow" for seat in self.seats: player_str = seat.player_name if state == "yellow": name_color_code = "^Y" score_color_code = "^C" state = "magenta" elif state == "magenta": name_color_code = "^M" score_color_code = "^G" state = "yellow" tell_string = " ^R%s^~: %s%s^~, %s%s^~" % (seat, name_color_code, player_str, score_color_code, get_plural_str(seat.data.score, "point")) player.tell_cc(tell_string + "\n") player.tell_cc("\n") def send_scores(self): for player in self.channel.listeners: self.show_scores(player) def show_help(self, player): super(Set, self).show_help(player) player.tell_cc("\nSET SETUP PHASE:\n\n") player.tell_cc(" ^!columns^. <num> Set the maximum columns to <num> (7-9).\n") player.tell_cc(" ^!delay^. <sec> Set the autodeal delay to <sec> secs.\n") player.tell_cc(" ^!cards^. <num> Set the maximum card count to <num>.\n") player.tell_cc(" ^!borders^. on|off Set the borders on or off.\n") player.tell_cc(" ^!start^. Start the game.\n") player.tell_cc("\nSET PLAY:\n\n") player.tell_cc(" ^!l1^., ^!l2^., ^!l3^. Declare <l1>, <l2>, <l3> a set.\n") player.tell_cc(" ^!scores^. See the current scores.\n")
class Game(object): """The base Game class. Does a lot of the boring footwork that all games need to handle: adding players, generating the chat channel for the game, handling kibitzing and player replacement, and so on. In general, though, you want one of the subclasses of this class, either SeatedGame() or SeatlessGame(). """ def __init__(self, server, table_name): self.server = server self.channel = server.channel_manager.has_channel(table_name) if not self.channel: self.channel = self.server.channel_manager.add_channel(table_name, gameable=True, persistent=True) else: self.channel.persistent = True self.game_display_name = "Generic Game" self.game_name = "game" self.table_display_name = table_name self.table_name = table_name.lower() self.active = False self.private = False self.state = State("config") self.prefix = "(^RGame^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Override this next variable in your subclasses if you're not # done debugging them. self.debug = False def __repr__(self): return ("%s (%s)" % (self.table_display_name, self.game_display_name)) def log_pre(self, log_str): # This utility function logs with the proper prefix. self.server.log.log(self.log_prefix + log_str) def tell_pre(self, player, tell_str): # This utility function tells a player a line with the proper prefix. player.tell_cc(self.prefix + tell_str) def bc_pre(self, send_str): # This utility function sends a message to the game channel with the # proper prefix. self.channel.broadcast_cc(self.prefix + send_str) def handle(self, player, command_str): # The generic handle does very little work; it passes it all off # to the common command handler. You are not expected to # actually /call/ this handle(), but if you do it has the same # effect as calling handle_common_commands(). self.handle_common_commands(player, command_str) def show_help(self, player): self.log_pre("%s asked for help with the game." % player) player.tell_cc("\nVIEWING:\n\n") player.tell_cc(" ^!kibitz^., ^!watch^. Watch the game as it happens.\n") player.tell_cc(" ^!show^., ^!look^., ^!l^. Look at the game itself.\n") player.tell_cc("\nPARTICIPATING:\n\n") player.tell_cc(" ^!terminate^., ^!finish^. Terminate game.\n") if self.debug: player.tell_cc("\nDEBUG:\n\n") player.tell_cc(" ^!change_state^. <state> Change game state to <state>.\n") def show(self, player): # This function should /absolutely/ be overridden by any games. self.tell_pre(player, "This is the default game class; nothing to show.\n") def finish(self): # If you have fancy cleanup that should be done when a game is # done, override this function. self.channel.persistent = False self.state.set("finished") def terminate(self, player): self.bc_pre("^Y%s^~ has terminated the game.\n" % player) self.log_pre("%s has terminated the game." % player) self.finish() def tick(self): # If your game has events that occur potentially without player # intervention, override this class. An obvious example is a # game with a timer; a less-obvious one is a game that you want # to auto-transition whenever certain conditions are met, such # as a game auto-starting when all the players are ready and # available. pass def remove_player(self, player): """Signature for removing a player from the game. When a player removes themselves from a game or disconnects from the server, this method is called on every game currently running; implementations are expected to only remove the player from a game if they are participating. """ # You will almost certainly want to override this if you're # writing a new subclass of Game(). Existing subclasses # may or may not have useful implementations extant. pass def handle_common_commands(self, player, command_str): # This handles certain command bits common to all games. # - If the game is finished, reject commands. # - At any point, take these generic commands: # * help (print help in regards to the game) # * kibitz (watch the game) # * show (show the game itself) # * terminate (end the game immediately) # * private (make private) # * public (make public) # - In addition, if we're in debug mode, allow people to # forcibly switch states via change_state. # # We also return whether or not we handled the command, which may # be useful to games that call us. handled = False # Pull out the command bits. command_bits = command_str.split() primary = command_bits[0].lower() # You can always ask for help... if primary in ('help', 'h', '?'): self.show_help(player) handled = True # You can always add yourself as a kibitzer... elif primary in ('kibitz', 'watch'): if not self.channel.is_connected(player): self.channel.connect(player) self.show(player) else: self.tell_pre(player, "You're already watching this game!\n") handled = True elif primary in ('show', 'look', 'l'): self.show(player) handled = True elif primary in ('terminate', 'finish', 'flip'): self.terminate(player) handled = True elif primary in ('private',): self.bc_pre("^R%s^~ has turned the game ^cprivate^~.\n" % (player)) self.private = True handled = True elif primary in ('public',): self.bc_pre("^R%s^~ has turned the game ^Cpublic^~.\n" % (player)) self.private = False handled = True elif primary in ('change_state',): if not self.debug: self.tell_pre(player, "No switching states in production!\n") elif len(command_bits) != 2: self.tell_pre(player, "Invalid state to switch to.\n") else: self.state.set(command_bits[1].lower()) self.bc_pre("^R%s^~ forced a state change to ^C%s^~.\n" % (player, self.state.get())) handled = True return handled
class Breakthrough(SeatedGame): """A Breakthrough game table implementation. Invented in 2000 by Dan Troyka. """ def __init__(self, server, table_name): super(Breakthrough, self).__init__(server, table_name) self.game_display_name = "Breakthrough" self.game_name = "breakthrough" self.seats = [ Seat("Black"), Seat("White") ] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.prefix = "(^RBreakthrough^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Breakthrough-specific stuff. self.width = 8 self.height = 8 self.turn = None self.black = self.seats[0] self.white = self.seats[1] self.resigner = None self.layout = None # We cheat and create a black and white piece here. Breakthrough # doesn't differentiate between pieces, so this allows us to do # comparisons later. self.bp = Piece("^K", "b", "B") self.bp.data.owner = self.black self.wp = Piece("^W", "w", "W") self.wp.data.owner = self.white self.init_board() def init_board(self): # Create the layout and the pieces it uses as well. Note that we # can be ultra-lazy with the pieces, as Breakthrough doesn't distinguish # between them, so there's no reason to create a bunch of identical # bits. self.layout = SquareGridLayout() self.layout.resize(self.width, self.height) last_row = self.height - 1 next_last_row = last_row - 1 for i in range(self.width): self.layout.place(self.bp, 0, i, update=False) self.layout.place(self.bp, 1, i, update=False) self.layout.place(self.wp, next_last_row, i, update=False) self.layout.place(self.wp, last_row, i, update=False) # Set the piece counts. self.white.data.piece_count = self.width * 2 self.black.data.piece_count = self.width * 2 self.layout.update() def show(self, player): player.tell_cc(self.layout) player.tell_cc(self.get_turn_str() + "\n") def send_board(self): for player in self.channel.listeners: self.show(player) def get_turn_str(self): if not self.turn: return ("The game has not yet started.\n") if self.turn == self.black: player = self.seats[0].player color_msg = "^KBlack^~" else: player = self.seats[1].player color_msg = "^WWhite^~" return ("It is ^Y%s^~'s turn (%s)." % (player, color_msg)) def move(self, player, src, dst): seat = self.get_seat_of_player(player) if not seat: self.tell_pre(player, "You can't move; you're not playing!\n") return False if self.turn != seat: self.tell_pre(player, "You must wait for your turn to move.\n") return False src_c, src_r = src dst_c, dst_r = dst src_str = "%s%s" % (COLS[src_c], src_r + 1) dst_str = "%s%s" % (COLS[dst_c], dst_r + 1) # Make sure they're all in range. if (not self.layout.is_valid(src_r, src_c) or not self.layout.is_valid(dst_r, dst_c)): self.tell_pre(player, "Your move is out of bounds.\n") return False # Does the player even have a piece there? src_loc = self.layout.grid[src_r][src_c] if not src_loc or src_loc.data.owner != self.turn: self.tell_pre(player, "You don't have a piece at ^C%s^~.\n" % src_str) return False # Is the destination within range? if self.turn == self.black: row_delta = 1 else: row_delta = -1 if src_r + row_delta != dst_r: self.tell_pre(player, "You can't move from ^C%s^~ to row ^R%d^~.\n" % (src_str, dst_r + 1)) return False if abs(src_c - dst_c) > 1: self.tell_pre(player, "You can't move from ^C%s^~ to column ^R%s^~.\n" % (src_str, COLS[dst_c])) return False # Okay, this is actually (gasp) a potentially legitimate move. If # it's a move forward, it only works if the forward space is empty. if src_c == dst_c and self.layout.grid[dst_r][dst_c]: self.tell_pre(player, "A straight-forward move can only be into an empty space.\n") return False # Otherwise, it must not have one of the player's own pieces in it. dst_loc = self.layout.grid[dst_r][dst_c] if src_loc == dst_loc: self.tell_pre(player, "A diagonal-forward move cannot be onto your own piece.\n") return False additional_str = "" opponent = self.seats[0] if seat == self.seats[0]: opponent = self.seats[1] if dst_loc: # It's a capture. additional_str = ", capturing one of ^R%s^~'s pieces" % (opponent.player) opponent.data.piece_count -= 1 self.bc_pre("^Y%s^~ moves a piece from ^C%s^~ to ^G%s^~%s.\n" % (seat.player, src_str, dst_str, additional_str)) # Make the move on the layout. self.layout.move(src_r, src_c, dst_r, dst_c, True) return ((src_r, src_c), (dst_r, dst_c)) 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.bc_pre("^KBlack^~: ^R%s^~; ^WWhite^~: ^Y%s^~\n" % (self.seats[0].player, self.seats[1].player)) self.turn = self.black self.send_board() def set_size(self, player, size_bits): # Is there an 'x' in the middle of a single argument? if len(size_bits) == 1: size_bits[0] = size_bits[0].lower() if "x" in size_bits[0]: size_bits = size_bits[0].split("x") width = size_bits[0] # If there's a single element, height == width. if len(size_bits) == 1: height = width elif len(size_bits) == 2: width = size_bits[0] height = size_bits[1] else: self.tell_pre(player, "Invalid size command.\n") return if not width.isdigit() or not height.isdigit(): self.tell_pre(player, "Invalid size command.\n") return w = int(width) h = int(height) if w < MIN_WIDTH or w > MAX_WIDTH: self.tell_pre(player, "Width must be between %d and %d inclusive.\n" % (MIN_WIDTH, MAX_WIDTH)) return if h < MIN_HEIGHT or h > MAX_HEIGHT: self.tell_pre(player, "Height must be between %d and %d inclusive.\n" % (MIN_HEIGHT, MAX_HEIGHT)) return # Valid! self.width = w self.height = h self.bc_pre("^R%s^~ has set the board size to ^C%d^Gx^C%d^~.\n" % (player, w, h)) self.init_board() 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("^R%s^~ is resigning from the game.\n" % player) 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.split() primary = command_bits[0] if state == "setup": if primary in ("size", "sz",): self.set_size(player, command_bits[1:]) handled = True if 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 ("move", "play", "mv", "pl",): invalid = False move_bits = demangle_move(command_bits[1:]) if move_bits and len(move_bits) == 2: made_move = self.move(player, move_bits[0], move_bits[1]) else: invalid = True if invalid: self.tell_pre(player, "Invalid move command.\n") handled = True elif primary in ("resign",): if self.resign(player): made_move = True handled = True if made_move: # Did someone win? winner = self.find_winner() if winner: self.resolve(winner) self.finish() else: # Nope. Switch turns... self.turn = self.next_seat(self.turn) # ...show everyone the board, and keep on. self.send_board() if not handled: self.tell_pre(player, "Invalid command.\n") def find_winner(self): # If someone resigned, this is the easiest thing ever. Same if # they lost all their pieces. if self.resigner == self.white or self.seats[1].data.piece_count == 0: return self.seats[0].player_name elif self.resigner == self.black or self.seats[1].data.piece_count == 0: return self.seats[1].player_name # Aw, we have to do work. If black has a piece on the last row, # they win; if white has a piece on the first row, they win. if self.bp in self.layout.grid[-1]: return self.seats[0].player_name if self.wp in self.layout.grid[0]: return self.seats[1].player_name # ...that wasn't really much work, but there's no winner yet. return None def resolve(self, winner): self.send_board() self.bc_pre("^C%s^~ wins!\n" % winner) def show_help(self, player): super(Breakthrough, self).show_help(player) player.tell_cc("\nBREAKTHROUGH SETUP PHASE:\n\n") player.tell_cc(" ^!setup^., ^!config^., ^!conf^. Enter setup phase.\n") player.tell_cc(" ^!size^. <size> | <w> <h>, ^!sz^. Set board to <size>x<size>/<w>x<h>.\n") player.tell_cc(" ^!ready^., ^!done^., ^!r^., ^!d^. End setup phase.\n") player.tell_cc("\nBREAKTHROUGH PLAY:\n\n") player.tell_cc(" ^!move^. <ln> <ln2>, ^!mv^. Move from <ln> to <ln2> (letter number).\n") player.tell_cc(" ^!resign^. Resign.\n")
class Redstone(SeatedGame): """A Redstone game table implementation. Invented in 2012 by Mark Steere. """ def __init__(self, server, table_name): super(Redstone, self).__init__(server, table_name) self.game_display_name = "Redstone" self.game_name = "redstone" self.seats = [ Seat("Black"), Seat("White"), ] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.prefix = "(^RRedstone^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Redstone-specific stuff. self.height = 19 self.width = 19 self.turn = None self.black = self.seats[0] self.black.data.seat_str = "^KBlack^~" self.black.data.made_move = False self.white = self.seats[1] self.white.data.seat_str = "^WWhite^~" self.white.data.made_move = False self.resigner = None self.layout = None # Like most abstracts, Redstone doesn't need to differentiate between # the pieces on the board. self.bp = Piece("^K", "x", "X") self.bp.data.owner = self.black self.black.data.piece = self.bp self.wp = Piece("^W", "o", "O") self.wp.data.owner = self.white self.white.data.piece = self.wp self.rp = Piece("^R", "r", "R") self.rp.data.owner = None # Initialize the starting layout. self.init_layout() def init_layout(self): # Create the layout. Empty, so easy. self.layout = SquareGridLayout(highlight_color="^I") self.layout.resize(self.width, self.height) def get_sp_str(self, seat): return "^C%s^~ (%s)" % (seat.player_name, seat.data.seat_str) def get_turn_str(self): if not self.turn: return "The game has not yet started.\n" return "It is ^C%s^~'s turn (%s).\n" % (self.turn.player_name, self.turn.data.seat_str) def show(self, player): player.tell_cc(self.layout) player.tell_cc(self.get_turn_str()) def send_board(self): for player in self.channel.listeners: self.show(player) def set_size(self, player, size_bits): # Is there an 'x' in the middle of a single argument? if len(size_bits) == 1: size_bits[0] = size_bits[0].lower() if "x" in size_bits[0]: size_bits = size_bits[0].split("x") width = size_bits[0] # If there's a single element, height == width. if len(size_bits) == 1: height = width elif len(size_bits) == 2: width = size_bits[0] height = size_bits[1] else: self.tell_pre(player, "Invalid size command.\n") return if not width.isdigit() or not height.isdigit(): self.tell_pre(player, "You didn't even send numbers!\n") return w = int(width) h = int(height) if w < MIN_SIZE or w > MAX_SIZE or h < MIN_SIZE or h > MAX_SIZE: self.tell_pre( player, "Width and height must be between %d and %d inclusive.\n" % (MIN_SIZE, MAX_SIZE)) return # Valid! self.width = w self.height = h self.bc_pre("^R%s^~ has set the board size to ^C%d^Gx^C%d^~.\n" % (player, w, h)) self.init_layout() def recurse_capture(self, seat, row, col, visited): # If it's a dud coordinate, bail. if not self.layout.is_valid(row, col): return None # If we've been here, bail. if visited[row][col]: return None # If it's an empty space, then it's a liberty. pos = self.layout.grid[row][col] if not pos: return [] # If it's a piece of the other player, bail. if pos.data.owner != seat: return None # Okay. New piece, right color. Mark it visited. visited[row][col] = True # Recurse on adjacencies. If any return an empty list, this group has # a liberty and we return an empty list as well; otherwise we return a # concatenation of pieces found further on, for easy removal. return_list = [(row, col)] for r_delta, c_delta in CONNECTION_DELTAS: result = self.recurse_capture(seat, row + r_delta, col + c_delta, visited) if result == []: # Liberty! Bail. return [] elif result: # Found a subgroup with no liberties. Extend. return_list.extend(result) # We never found a liberty. Return the list of pieces. return return_list def move_is_capture(self, piece, row, col): # Tentatively place the piece here. self.layout.place(piece, row, col, update=False) # Build the visitation list. visited = [] for r in range(self.height): visited.append([None] * self.width) # If this piece is not a redstone, we check its own liberties. We # can quickly bail if this succeeds. if piece != self.rp: if self.recurse_capture(piece.data.owner, row, col, visited): self.layout.remove(row, col, update=False) return True # Now we check the liberties of all four adjacent locations, assuming # there's a piece there and it's not a redstone. for r_delta, c_delta in CONNECTION_DELTAS: new_r = row + r_delta new_c = col + c_delta if self.layout.is_valid(new_r, new_c): pos = self.layout.grid[new_r][new_c] if pos and pos != self.rp: # We have to rebuild the visited list for each piece we # check, because of the recursive "bail fast" method we use # for detecting liberties. This should be improved. visited = [] for r in range(self.height): visited.append([None] * self.width) if self.recurse_capture(pos.data.owner, new_r, new_c, visited): # Bail. self.layout.remove(row, col, update=False) return True # We never found a capture. Remove and return. self.layout.remove(row, col, update=False) return False def move(self, player, move_bits): seat = self.get_seat_of_player(player) if not seat: self.tell_pre(player, "You can't move; you're not playing!\n") return False if seat != self.turn: self.tell_pre(player, "You must wait for your turn to move.\n") return False col, row = move_bits # Is the move on the board? if not self.layout.is_valid(row, col): self.tell_pre(player, "Your move is out of bounds.\n") return False # Is there a piece already there? if self.layout.grid[row][col]: self.tell_pre(player, "There is already a piece there.\n") return False # Is it a capturing move? piece = seat.data.piece if self.move_is_capture(piece, row, col): self.tell_pre(player, "That would cause a capture.\n") return False # Valid. Put a piece there. move_str = "%s%s" % (COLS[col], row + 1) self.layout.place(piece, row, col, True) # Update the board. self.bc_pre("%s places a piece at ^C%s^~.\n" % (self.get_sp_str(seat), move_str)) seat.data.made_move = True return True def capture(self, row, col): # If for some reason this is called and there's not a redstone at this # location, bail. if self.layout.grid[row][col] != self.rp: return -1 # All right. Check all four adjacencies; if they no longer have a # liberty, capture them. Note that we come up with the list first, # and /then/ execute the captures, as doing them as we find them may # give groups liberties during the removal process. visited = [] for r in range(self.height): visited.append([None] * self.width) capture_list = [] for r_delta, c_delta in CONNECTION_DELTAS: new_r = row + r_delta new_c = col + c_delta if self.layout.is_valid(new_r, new_c): loc = self.layout.grid[new_r][new_c] if loc and loc != self.rp: captures = self.recurse_capture(loc.data.owner, new_r, new_c, visited) if captures: capture_list.extend( [x for x in captures if x not in capture_list]) # Remove all pieces in the capture list. for capture_r, capture_c in capture_list: self.layout.remove(capture_r, capture_c, update=False) self.layout.update() # Return the number of pieces captured. return len(capture_list) def red(self, player, move_bits): seat = self.get_seat_of_player(player) if not seat: self.tell_pre(player, "You can't move; you're not playing!\n") return False if seat != self.turn: self.tell_pre(player, "You must wait for your turn to move.\n") return False col, row = move_bits # Is the move on the board? if not self.layout.is_valid(row, col): self.tell_pre(player, "Your move is out of bounds.\n") return False # Is there a piece already there? if self.layout.grid[row][col]: self.tell_pre(player, "There is already a piece there.\n") return False # Is it not a capturing move? piece = self.rp if not self.move_is_capture(piece, row, col): self.tell_pre(player, "That would not cause a capture.\n") return False # Valid. Put the piece there. move_str = "%s%s" % (COLS[col], row + 1) self.layout.place(piece, row, col, True) # Redstones by definition make captures. capture_count = self.capture(row, col) self.bc_pre("%s places a ^Rredstone^~ at ^C%s^~, ^Ycapturing %s^~.\n" % (self.get_sp_str(seat), move_str, get_plural_str(capture_count, "stone"))) seat.data.made_move = True 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 seat != self.turn: 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 tick(self): # If both seats are occupied and the game is active, start. if (self.state.get() == "need_players" and self.black.player and self.white.player and self.active): self.bc_pre("%s: ^C%s^~; %s: ^C%s^~\n" % (self.black.data.seat_str, self.black.player_name, self.white.data.seat_str, self.white.player_name)) self.state.set("playing") self.turn = self.black self.send_board() 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 ("size", "sz"): self.set_size(player, command_bits[1:]) 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.bc_pre( "^R%s^~ has switched the game to setup mode.\n" % player) self.state.set("setup") handled = True elif state == "playing": made_move = False if primary in ( "move", "play", "mv", "pl", ): move_bits = demangle_move(command_bits[1:]) if move_bits and len(move_bits) == 1: made_move = self.move(player, move_bits[0]) else: self.tell_pre(player, "Invalid move command.\n") handled = True if primary in ( "redstone", "red", "r", ): move_bits = demangle_move(command_bits[1:]) if move_bits and len(move_bits) == 1: made_move = self.red(player, move_bits[0]) else: self.tell_pre(player, "Invalid red command.\n") handled = True elif primary in ("resign", ): made_move = self.resign(player) handled = True if made_move: # Did someone win? winner = self.find_winner() if winner: # Yup! self.resolve(winner) self.finish() else: # No. Switch turns. self.turn = self.next_seat(self.turn) self.send_board() if not handled: self.tell_pre(player, "Invalid command.\n") def find_winner(self): # Did someone resign? if self.resigner == self.white: return self.black elif self.resigner == self.black: return self.white # If one player has no pieces left, the other player won. If neither # player has a piece, mover wins. found_white = False found_black = False for r in range(self.height): for c in range(self.width): loc = self.layout.grid[r][c] if loc: if loc.data.owner == self.black: found_black = True elif loc.data.owner == self.white: found_white = True if not found_black and self.black.data.made_move: if not found_white and self.white.data.made_move: # Mover wins. return self.turn else: return self.white elif not found_white and self.white.data.made_move: return self.black # No winner yet. return None def resolve(self, winner): self.send_board() self.bc_pre("%s wins!\n" % self.get_sp_str(winner)) def show_help(self, player): super(Redstone, self).show_help(player) player.tell_cc("\nREDSTONE SETUP PHASE:\n\n") player.tell_cc( " ^!setup^., ^!config^., ^!conf^. Enter setup phase.\n" ) player.tell_cc( " ^!size^. <size>, ^!sz^. Set board to <size>.\n") player.tell_cc( " ^!ready^., ^!done^., ^!r^., ^!d^. End setup phase.\n" ) player.tell_cc("\nREDSTONE PLAY:\n\n") player.tell_cc( " ^!move^. <ln>, ^!play^., ^!mv^., ^!pl^. Make move <ln> (letter number).\n" ) player.tell_cc( " ^!red^. <ln>, ^!r^. Place redstone at <ln> (letter number).\n" ) player.tell_cc(" ^!resign^. Resign.\n")
class Breakthrough(SeatedGame): """A Breakthrough game table implementation. Invented in 2000 by Dan Troyka. """ def __init__(self, server, table_name): super(Breakthrough, self).__init__(server, table_name) self.game_display_name = "Breakthrough" self.game_name = "breakthrough" self.seats = [Seat("Black"), Seat("White")] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.prefix = "(^RBreakthrough^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Breakthrough-specific stuff. self.width = 8 self.height = 8 self.turn = None self.black = self.seats[0] self.white = self.seats[1] self.resigner = None self.layout = None # We cheat and create a black and white piece here. Breakthrough # doesn't differentiate between pieces, so this allows us to do # comparisons later. self.bp = Piece("^K", "b", "B") self.bp.data.owner = self.black self.wp = Piece("^W", "w", "W") self.wp.data.owner = self.white self.init_board() def init_board(self): # Create the layout and the pieces it uses as well. Note that we # can be ultra-lazy with the pieces, as Breakthrough doesn't distinguish # between them, so there's no reason to create a bunch of identical # bits. self.layout = SquareGridLayout() self.layout.resize(self.width, self.height) last_row = self.height - 1 next_last_row = last_row - 1 for i in range(self.width): self.layout.place(self.bp, 0, i, update=False) self.layout.place(self.bp, 1, i, update=False) self.layout.place(self.wp, next_last_row, i, update=False) self.layout.place(self.wp, last_row, i, update=False) # Set the piece counts. self.white.data.piece_count = self.width * 2 self.black.data.piece_count = self.width * 2 self.layout.update() def show(self, player): player.tell_cc(self.layout) player.tell_cc(self.get_turn_str() + "\n") def send_board(self): for player in self.channel.listeners: self.show(player) def get_turn_str(self): if not self.turn: return ("The game has not yet started.\n") if self.turn == self.black: player = self.seats[0].player color_msg = "^KBlack^~" else: player = self.seats[1].player color_msg = "^WWhite^~" return ("It is ^Y%s^~'s turn (%s)." % (player, color_msg)) def move(self, player, src, dst): seat = self.get_seat_of_player(player) if not seat: self.tell_pre(player, "You can't move; you're not playing!\n") return False if self.turn != seat: self.tell_pre(player, "You must wait for your turn to move.\n") return False src_c, src_r = src dst_c, dst_r = dst src_str = "%s%s" % (COLS[src_c], src_r + 1) dst_str = "%s%s" % (COLS[dst_c], dst_r + 1) # Make sure they're all in range. if (not self.layout.is_valid(src_r, src_c) or not self.layout.is_valid(dst_r, dst_c)): self.tell_pre(player, "Your move is out of bounds.\n") return False # Does the player even have a piece there? src_loc = self.layout.grid[src_r][src_c] if not src_loc or src_loc.data.owner != self.turn: self.tell_pre(player, "You don't have a piece at ^C%s^~.\n" % src_str) return False # Is the destination within range? if self.turn == self.black: row_delta = 1 else: row_delta = -1 if src_r + row_delta != dst_r: self.tell_pre( player, "You can't move from ^C%s^~ to row ^R%d^~.\n" % (src_str, dst_r + 1)) return False if abs(src_c - dst_c) > 1: self.tell_pre( player, "You can't move from ^C%s^~ to column ^R%s^~.\n" % (src_str, COLS[dst_c])) return False # Okay, this is actually (gasp) a potentially legitimate move. If # it's a move forward, it only works if the forward space is empty. if src_c == dst_c and self.layout.grid[dst_r][dst_c]: self.tell_pre( player, "A straight-forward move can only be into an empty space.\n") return False # Otherwise, it must not have one of the player's own pieces in it. dst_loc = self.layout.grid[dst_r][dst_c] if src_loc == dst_loc: self.tell_pre( player, "A diagonal-forward move cannot be onto your own piece.\n") return False additional_str = "" opponent = self.seats[0] if seat == self.seats[0]: opponent = self.seats[1] if dst_loc: # It's a capture. additional_str = ", capturing one of ^R%s^~'s pieces" % ( opponent.player) opponent.data.piece_count -= 1 self.bc_pre("^Y%s^~ moves a piece from ^C%s^~ to ^G%s^~%s.\n" % (seat.player, src_str, dst_str, additional_str)) # Make the move on the layout. self.layout.move(src_r, src_c, dst_r, dst_c, True) return ((src_r, src_c), (dst_r, dst_c)) 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.bc_pre("^KBlack^~: ^R%s^~; ^WWhite^~: ^Y%s^~\n" % (self.seats[0].player, self.seats[1].player)) self.turn = self.black self.send_board() def set_size(self, player, size_bits): # Is there an 'x' in the middle of a single argument? if len(size_bits) == 1: size_bits[0] = size_bits[0].lower() if "x" in size_bits[0]: size_bits = size_bits[0].split("x") width = size_bits[0] # If there's a single element, height == width. if len(size_bits) == 1: height = width elif len(size_bits) == 2: width = size_bits[0] height = size_bits[1] else: self.tell_pre(player, "Invalid size command.\n") return if not width.isdigit() or not height.isdigit(): self.tell_pre(player, "Invalid size command.\n") return w = int(width) h = int(height) if w < MIN_WIDTH or w > MAX_WIDTH: self.tell_pre( player, "Width must be between %d and %d inclusive.\n" % (MIN_WIDTH, MAX_WIDTH)) return if h < MIN_HEIGHT or h > MAX_HEIGHT: self.tell_pre( player, "Height must be between %d and %d inclusive.\n" % (MIN_HEIGHT, MAX_HEIGHT)) return # Valid! self.width = w self.height = h self.bc_pre("^R%s^~ has set the board size to ^C%d^Gx^C%d^~.\n" % (player, w, h)) self.init_board() 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("^R%s^~ is resigning from the game.\n" % player) 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.split() primary = command_bits[0] if state == "setup": if primary in ( "size", "sz", ): self.set_size(player, command_bits[1:]) handled = True if 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 ( "move", "play", "mv", "pl", ): invalid = False move_bits = demangle_move(command_bits[1:]) if move_bits and len(move_bits) == 2: made_move = self.move(player, move_bits[0], move_bits[1]) else: invalid = True if invalid: self.tell_pre(player, "Invalid move command.\n") handled = True elif primary in ("resign", ): if self.resign(player): made_move = True handled = True if made_move: # Did someone win? winner = self.find_winner() if winner: self.resolve(winner) self.finish() else: # Nope. Switch turns... self.turn = self.next_seat(self.turn) # ...show everyone the board, and keep on. self.send_board() if not handled: self.tell_pre(player, "Invalid command.\n") def find_winner(self): # If someone resigned, this is the easiest thing ever. Same if # they lost all their pieces. if self.resigner == self.white or self.seats[1].data.piece_count == 0: return self.seats[0].player_name elif self.resigner == self.black or self.seats[1].data.piece_count == 0: return self.seats[1].player_name # Aw, we have to do work. If black has a piece on the last row, # they win; if white has a piece on the first row, they win. if self.bp in self.layout.grid[-1]: return self.seats[0].player_name if self.wp in self.layout.grid[0]: return self.seats[1].player_name # ...that wasn't really much work, but there's no winner yet. return None def resolve(self, winner): self.send_board() self.bc_pre("^C%s^~ wins!\n" % winner) def show_help(self, player): super(Breakthrough, self).show_help(player) player.tell_cc("\nBREAKTHROUGH SETUP PHASE:\n\n") player.tell_cc( " ^!setup^., ^!config^., ^!conf^. Enter setup phase.\n" ) player.tell_cc( " ^!size^. <size> | <w> <h>, ^!sz^. Set board to <size>x<size>/<w>x<h>.\n" ) player.tell_cc( " ^!ready^., ^!done^., ^!r^., ^!d^. End setup phase.\n" ) player.tell_cc("\nBREAKTHROUGH PLAY:\n\n") player.tell_cc( " ^!move^. <ln> <ln2>, ^!mv^. Move from <ln> to <ln2> (letter number).\n" ) player.tell_cc(" ^!resign^. Resign.\n")
class Tanbo(SeatedGame): """A Tanbo game table implementation. Invented in 1993 by Mark Steere. This only implements the 2p version, although it does have the 9x9, 13x13, and 19x19 sizes. There's also a 21x21 size that came from discussion with Mark, and 5x5 and 7x7 sizes that came naturally from the piece layouts. """ def __init__(self, server, table_name): super(Tanbo, self).__init__(server, table_name) self.game_display_name = "Tanbo" self.game_name = "tanbo" self.seats = [ Seat("Black"), Seat("White"), ] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.prefix = "(^RTanbo^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Tanbo-specific stuff. self.size = 19 self.turn = None self.black = self.seats[0] self.black.data.seat_str = "^KBlack^~" self.black.data.root_list = [] self.white = self.seats[1] self.white.data.seat_str = "^WWhite^~" self.white.data.root_list = [] self.resigner = None self.layout = None # Initialize the starting layout. self.init_layout() def get_root_piece(self, seat, num): if seat == self.black: p = Piece("^K", "x", "X") else: p = Piece("^W", "o", "O") p.data.owner = seat p.data.num = num return p def init_layout(self): # Create the layout and fill it with pieces. self.layout = SquareGridLayout(highlight_color="^I") self.layout.resize(self.size) black_count = 0 white_count = 0 self.black.data.root_list = [] self.white.data.root_list = [] # There are three different layouts depending on the size. All of them # alternate between black and white pieces; the two larger ones have 16 # roots, whereas the smallest size has 4. (There's a 5x5 grid for # testing as well.) if self.size == 5: jump_delta = 4 extent = 2 offset = 0 elif self.size == 7: jump_delta = 6 extent = 2 offset = 0 elif self.size == 9: jump_delta = 6 extent = 2 offset = 1 elif self.size == 13: jump_delta = 4 extent = 4 offset = 0 elif self.size == 19: jump_delta = 6 extent = 4 offset = 0 else: # size == 21 jump_delta = 4 extent = 6 offset = 0 for i in range(extent): for j in range(extent): if (i + j) % 2: p = self.get_root_piece(self.black, black_count) self.black.data.root_list.append(p) black_count += 1 else: p = self.get_root_piece(self.white, white_count) self.white.data.root_list.append(p) white_count += 1 row = offset + i * jump_delta col = offset + j * jump_delta p.data.start = (row, col) self.layout.place(p, row, col, update=False) self.layout.update() def get_sp_str(self, seat): return "^C%s^~ (%s)" % (seat.player_name, seat.data.seat_str) def get_turn_str(self): if not self.turn: return "The game has not yet started.\n" return "It is ^C%s^~'s turn (%s).\n" % (self.turn.player_name, self.turn.data.seat_str) def show(self, player): player.tell_cc(self.layout) player.tell_cc(self.get_turn_str()) def send_board(self): for player in self.channel.listeners: self.show(player) def set_size(self, player, size_str): if not size_str.isdigit(): self.tell_pre(player, "You didn't even send a number!\n") return False size = int(size_str) if size not in ( 5, 7, 9, 13, 19, 21, ): self.tell_pre(player, "Size must be 5, 7, 9, 13, 19, or 21.\n") return False # Valid! self.size = size self.bc_pre("^R%s^~ has set the board size to ^C%d^~.\n" % (player, size)) self.init_layout() def can_place_at(self, seat, row, col): # You can place a piece in Tanbo iff it is adjacent to exactly one of # your own pieces. If a location is valid, we'll return the piece that # is adjacent; otherwise we return nothing. if self.layout.grid[row][col]: # Occupied; clearly can't place here. return None adj_count = 0 for r_delta, c_delta in CONNECTION_DELTAS: new_r = row + r_delta new_c = col + c_delta if self.layout.is_valid(new_r, new_c): loc = self.layout.grid[new_r][new_c] if loc and loc.data.owner == seat: piece = loc adj_count += 1 if adj_count == 1: return piece else: return None def recurse_is_bound(self, piece, row, col, prev_row, prev_col): # Thanks to the way Tanbo placement works, we don't have to worry about # loops, so we can get away with not using an adjacenty map and instead # just track the direction we came from. # # Bail if this isn't a valid location. if not self.layout.is_valid(row, col): return True loc = self.layout.grid[row][col] # If there's no piece here, see if we can place one. if not loc: if self.can_place_at(piece.data.owner, row, col): # Yup. This root isn't bound. return False else: # No, this location is binding. return True elif loc != piece: # Some other root. Definitely binding. return True else: # Okay, it's another part of this root. Recurse, but don't double # back. for r_delta, c_delta in CONNECTION_DELTAS: new_r = row + r_delta new_c = col + c_delta if new_r != prev_row or new_c != prev_col: if not self.recurse_is_bound(piece, new_r, new_c, row, col): # A recursive call found a liberty. Awesome! return False # All of the recursive calls returned that they were bound. This # (sub)root is bound. return True def root_is_bound(self, piece): # We'll just start recursing at the root's starting location and find # whether it's bound or not. row, col = piece.data.start return self.recurse_is_bound(piece, row, col, None, None) def kill_root(self, piece): # We could do this recursively, but we're lazy. for r in range(self.size): for c in range(self.size): loc = self.layout.grid[r][c] if loc == piece: self.layout.remove(r, c, update=False) self.layout.update() # Remove this root from the owner's root list. piece.data.owner.data.root_list.remove(piece) def update_roots(self, row, col): # If the piece at row, col is part of a bounded root, that root is killed. piece = self.layout.grid[row][col] if self.root_is_bound(piece): self.kill_root(piece) # -1 indicates a suicide. return -1 # Not a suicide; loop through all roots, finding bound ones. bound_root_list = [] all_roots = self.black.data.root_list[:] all_roots.extend(self.white.data.root_list) for root in all_roots: if self.root_is_bound(root): bound_root_list.append(root) bound_count = 0 for bound_root in bound_root_list: self.kill_root(bound_root) bound_count += 1 # Return the number of roots we killed. return bound_count def move(self, player, move_bits): seat = self.get_seat_of_player(player) if not seat: self.tell_pre(player, "You can't move; you're not playing!\n") return False if seat != self.turn: self.tell_pre(player, "You must wait for your turn to move.\n") return False col, row = move_bits # Is the move on the board? if not self.layout.is_valid(row, col): self.tell_pre(player, "Your move is out of bounds.\n") return False # Is it a valid Tanbo play? piece = self.can_place_at(seat, row, col) if not piece: self.tell_pre( player, "That location is not adjacent to exactly one of your pieces.\n" ) return False # Valid. Put the piece there. move_str = "%s%s" % (COLS[col], row + 1) self.layout.place(piece, row, col, True) # Update the root statuses. root_kill_str = "" root_kill = self.update_roots(row, col) if root_kill < 0: root_kill_str = ", ^ysuiciding the root^~" elif root_kill > 0: root_kill_str = ", ^Ykilling %s^~" % (get_plural_str( root_kill, "root")) self.bc_pre("%s grows a root to ^C%s^~%s.\n" % (self.get_sp_str(seat), move_str, root_kill_str)) 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 seat != self.turn: 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 tick(self): # If both seats are occupied and the game is active, start. if (self.state.get() == "need_players" and self.black.player and self.white.player and self.active): self.bc_pre("%s: ^C%s^~; %s: ^C%s^~\n" % (self.black.data.seat_str, self.black.player_name, self.white.data.seat_str, self.white.player_name)) self.state.set("playing") self.turn = self.black self.send_board() 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 ("size", "sz"): if len(command_bits) == 2: self.set_size(player, command_bits[1]) else: self.tell_pre(player, "Invalid size 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.bc_pre( "^R%s^~ has switched the game to setup mode.\n" % player) self.state.set("setup") handled = True elif state == "playing": made_move = False if primary in ( "move", "play", "mv", "pl", ): move_bits = demangle_move(command_bits[1:]) if move_bits and len(move_bits) == 1: made_move = self.move(player, move_bits[0]) else: self.tell_pre(player, "Invalid move command.\n") handled = True elif primary in ("resign", ): made_move = self.resign(player) handled = True if made_move: # Did someone win? winner = self.find_winner() if winner: # Yup! self.resolve(winner) self.finish() else: # No. Change turns and send the board to listeners. self.turn = self.next_seat(self.turn) self.send_board() if not handled: self.tell_pre(player, "Invalid command.\n") def find_winner(self): # Did someone resign? if self.resigner == self.white: return self.black elif self.resigner == self.black: return self.white # If one player has no pieces left, the other player won. if not len(self.white.data.root_list): return self.black elif not len(self.black.data.root_list): return self.white # No winner. return None def resolve(self, winner): self.send_board() self.bc_pre("%s wins!\n" % self.get_sp_str(winner)) def show_help(self, player): super(Tanbo, self).show_help(player) player.tell_cc("\nTANBO SETUP PHASE:\n\n") player.tell_cc( " ^!setup^., ^!config^., ^!conf^. Enter setup phase.\n" ) player.tell_cc( " ^!size^. 5|7|9|13|19|21, ^!sz^. Set board to <size>.\n") player.tell_cc( " ^!ready^., ^!done^., ^!r^., ^!d^. End setup phase.\n" ) player.tell_cc("\nTANBO PLAY:\n\n") player.tell_cc( " ^!move^. <ln>, ^!play^., ^!mv^., ^!pl^. Make move <ln> (letter number).\n" ) player.tell_cc(" ^!resign^. Resign.\n")
class RockPaperScissors(SeatedGame): """A Rock-Paper-Scissors game table implementation. """ def __init__(self, server, table_name): super(RockPaperScissors, self).__init__(server, table_name) self.game_display_name = "Rock-Paper-Scissors" self.game_name = "rps" self.seats = [ Seat("Left"), Seat("Right"), ] self.min_players = 2 self.max_players = 2 self.state = State("need_players") self.plays = [None, None] self.prefix = "(^RRPS^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # RPS requires both seats, so may as well mark them active. self.seats[0].active = True self.seats[1].active = True def handle(self, player, command_str): # Handle common commands. handled = self.handle_common_commands(player, command_str) state = self.state.get() if state == "need_moves": command_bits = command_str.split() primary = command_bits[0].lower() # If this player is used to prefacing plays with 'move'/'play', # let's be polite and just chomp that away. Also allow 'throw' # and 'th', even though they're undocumented, because they seem # like an obvious sort of command to try. (Read that as: I kept # typing it.) if primary in ('move', 'play', 'throw', 'mv', 'pl', 'th') and len(command_bits) > 1: primary = command_bits[1].lower() if primary in ('r', 'p', 's', 'rock', 'paper', 'scissors'): self.move(player, primary) handled = True if self.plays[0] and self.plays[1] and self.active: # Got the moves! self.resolve() self.finish() if not handled: player.tell_cc(self.prefix + "Invalid command.\n") def tick(self): # If we were looking for players, check to see if both # seats are full and the game is active. If so, autostart. if (self.state.get() == "need_players" and self.seats[0].player and self.seats[1].player and self.active): self.state.set("need_moves") self.channel.broadcast_cc( self.prefix + "Left: ^Y%s^~; Right: ^Y%s^~\n" % (self.seats[0].player, self.seats[1].player)) self.channel.broadcast_cc(self.prefix + "Players, make your moves!\n") def show(self, player): state = self.state.get() if state == "need_players": player.tell_cc( self.prefix + "Everyone is hovering around the table, waiting for players.\n" ) elif state == "need_moves": for loc, color in ((0, "^Y"), (1, "^M")): if self.seats[loc].player: name = repr(self.seats[loc].player) if self.plays[loc]: player.tell_cc( self.prefix + color + name + "^~'s hand is trembling with anticipation.\n") else: player.tell_cc(self.prefix + color + name + "^~ seems to be deep in thought.\n") else: player.tell_cc(self.prefix + "^C%s^~ is strangely empty.\n" % self.seats[loc]) else: player.tell_cc(self.prefix + "Nothing to see here. Move along.\n") def show_help(self, player): super(RockPaperScissors, self).show_help(player) player.tell_cc("\nROCK-PAPER-SCISSORS:\n\n") player.tell_cc( " ^!rock^., ^!r^. Throw rock.\n") player.tell_cc( " ^!paper^., ^!p^. Throw paper.\n") player.tell_cc( " ^!scissors^., ^!s^. Throw scissors.\n") def move(self, player, play): seat = self.get_seat_of_player(player) if not seat: player.tell_cc(self.prefix + "You're not playing in this game!\n") return if play in ('r', 'rock'): this_move = "rock" elif play in ('p', 'paper'): this_move = "paper" elif play in ('s', 'scissors'): this_move = "scissors" else: player.tell_cc(self.prefix + "Invalid play.\n") return self.channel.broadcast_cc(self.prefix + "%s's hand twitches.\n" % player) if seat == self.seats[0]: self.plays[0] = this_move else: self.plays[1] = this_move def resolve(self): one = self.plays[0] two = self.plays[1] one_name = "^Y" + repr(self.seats[0].player) + "^~" two_name = "^M" + repr(self.seats[1].player) + "^~" self.channel.broadcast_cc(self.prefix + "Jan... ken... pon... Throwdown time!\n") self.channel.broadcast_cc(self.prefix + "%s throws ^!%s^.; %s throws ^!%s^.!\n" % (one_name, one, two_name, two)) if one == two: msg = "It's a tie!\n" elif ((one == "rock" and two == "paper") or (one == "paper" and two == "scissors") or (one == "scissors" and two == "rock")): msg = two_name + " wins!\n" else: msg = one_name + " wins!\n" self.channel.broadcast_cc(msg) def remove_player(self, player): # Not only do we want to do the standard things, but if this person # really is a player, we want to invalidate their throw. That way # you're not stuck with another player's throw mid-game. if self.seats[0].player == player: self.plays[0] = None elif self.seats[1].player == player: self.plays[1] = None super(RockPaperScissors, self).remove_player(player)
class Game(object): """The base Game class. Does a lot of the boring footwork that all games need to handle: adding players, generating the chat channel for the game, handling kibitzing and player replacement, and so on. In general, though, you want one of the subclasses of this class, either SeatedGame() or SeatlessGame(). """ def __init__(self, server, table_name): self.server = server self.channel = server.channel_manager.has_channel(table_name) if not self.channel: self.channel = self.server.channel_manager.add_channel(table_name, gameable=True, persistent=True) else: self.channel.persistent = True self.game_display_name = "Generic Game" self.game_name = "game" self.table_display_name = table_name self.table_name = table_name.lower() self.active = False self.private = False self.state = State("config") self.prefix = "(^RGame^~): " self.log_prefix = "%s/%s: " % (self.table_display_name, self.game_display_name) # Override this next variable in your subclasses if you're not # done debugging them. self.debug = False def __repr__(self): return ("%s (%s)" % (self.table_display_name, self.game_display_name)) def log_pre(self, log_str): # This utility function logs with the proper prefix. self.server.log.log(self.log_prefix + log_str) def tell_pre(self, player, tell_str): # This utility function tells a player a line with the proper prefix. player.tell_cc(self.prefix + tell_str) def bc_pre(self, send_str): # This utility function sends a message to the game channel with the # proper prefix. self.channel.broadcast_cc(self.prefix + send_str) def handle(self, player, command_str): # The generic handle does very little work; it passes it all off # to the common command handler. You are not expected to # actually /call/ this handle(), but if you do it has the same # effect as calling handle_common_commands(). self.handle_common_commands(player, command_str) def show_help(self, player): self.log_pre("%s asked for help with the game." % player) player.tell_cc("\nVIEWING:\n\n") player.tell_cc(" ^!kibitz^., ^!watch^. Watch the game as it happens.\n") player.tell_cc(" ^!show^., ^!look^., ^!l^. Look at the game itself.\n") player.tell_cc("\nPARTICIPATING:\n\n") player.tell_cc(" ^!terminate^., ^!finish^. Terminate game.\n") if self.debug: player.tell_cc("\nDEBUG:\n\n") player.tell_cc(" ^!change_state^. <state> Change game state to <state>.\n") def show(self, player): # This function should /absolutely/ be overridden by any games. self.tell_pre(player, "This is the default game class; nothing to show.\n") def finish(self): # If you have fancy cleanup that should be done when a game is # done, override this function. self.log_pre("This game has been marked as finished.") self.channel.persistent = False self.state.set("finished") def terminate(self, player): self.bc_pre("^Y%s^~ has terminated the game.\n" % player) self.log_pre("%s has terminated the game." % player) self.finish() def tick(self): # If your game has events that occur potentially without player # intervention, override this class. An obvious example is a # game with a timer; a less-obvious one is a game that you want # to auto-transition whenever certain conditions are met, such # as a game auto-starting when all the players are ready and # available. pass def remove_player(self, player): """Signature for removing a player from the game. When a player removes themselves from a game or disconnects from the server, this method is called on every game currently running; implementations are expected to only remove the player from a game if they are participating. """ # You will almost certainly want to override this if you're # writing a new subclass of Game(). Existing subclasses # may or may not have useful implementations extant. pass def handle_common_commands(self, player, command_str): # This handles certain command bits common to all games. # - If the game is finished, reject commands. # - At any point, take these generic commands: # * help (print help in regards to the game) # * kibitz (watch the game) # * show (show the game itself) # * terminate (end the game immediately) # * private (make private) # * public (make public) # - In addition, if we're in debug mode, allow people to # forcibly switch states via change_state. # # We also return whether or not we handled the command, which may # be useful to games that call us. handled = False # Pull out the command bits. command_bits = command_str.split() primary = command_bits[0].lower() # You can always ask for help... if primary in ('help', 'h', '?'): self.show_help(player) handled = True # You can always add yourself as a kibitzer... elif primary in ('kibitz', 'watch'): if not self.channel.is_connected(player): self.channel.connect(player) self.show(player) else: self.tell_pre(player, "You're already watching this game!\n") handled = True elif primary in ('show', 'look', 'l'): self.show(player) handled = True elif primary in ('terminate', 'finish', 'flip'): self.terminate(player) handled = True elif primary in ('private',): self.bc_pre("^R%s^~ has turned the game ^cprivate^~.\n" % (player)) self.private = True handled = True elif primary in ('public',): self.bc_pre("^R%s^~ has turned the game ^Cpublic^~.\n" % (player)) self.private = False handled = True elif primary in ('change_state',): if not self.debug: self.tell_pre(player, "No switching states in production!\n") elif len(command_bits) != 2: self.tell_pre(player, "Invalid state to switch to.\n") else: self.state.set(command_bits[1].lower()) self.bc_pre("^R%s^~ forced a state change to ^C%s^~.\n" % (player, self.state.get())) handled = True return handled