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