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 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 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 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")