| from arena.board_view import to_svg | |
| from typing import List | |
| RED = 1 | |
| YELLOW = -1 | |
| EMPTY = 0 | |
| show = {EMPTY: "⚪️", RED: "🔴", YELLOW: "🟡"} | |
| pieces = {EMPTY: "", RED: "red", YELLOW: "yellow"} | |
| simple = {EMPTY: "_", RED: "R", YELLOW: "Y"} | |
| cols = "ABCDEFG" | |
| class Board: | |
| """ | |
| A class to represent a Four-in-the-row Board | |
| """ | |
| def __init__(self): | |
| """ | |
| Initialize this instance, starting with empty cells, RED to play | |
| The latest x,y is used to track the most recent move, so it animates on the display | |
| """ | |
| self.cells = [[0 for _ in range(7)] for _ in range(6)] | |
| self.player = RED | |
| self.winner = EMPTY | |
| self.draw = False | |
| self.forfeit = False | |
| self.latest_x, self.latest_y = -1, -1 | |
| def __repr__(self): | |
| """ | |
| A visual representation | |
| """ | |
| result = "" | |
| for y in range(6): | |
| for x in range(7): | |
| result += show[self.cells[5 - y][x]] | |
| result += "\n" | |
| result += "\n" + self.message() | |
| return result | |
| def message(self): | |
| """ | |
| A summary of the status | |
| """ | |
| if self.winner and self.forfeit: | |
| return f"{show[self.winner]} wins after an illegal move by {show[-1*self.winner]}\n" | |
| elif self.winner: | |
| return f"{show[self.winner]} wins\n" | |
| elif self.draw: | |
| return "The game is a draw\n" | |
| else: | |
| return f"{show[self.player]} to play\n" | |
| def html(self): | |
| """ | |
| Return an HTML representation | |
| """ | |
| result = '<div style="text-align: center;font-size:24px">' | |
| result += self.__repr__().replace("\n", "<br/>") | |
| result += "</div>" | |
| return result | |
| def svg(self): | |
| """ | |
| Return an SVG representation | |
| """ | |
| return to_svg(self) | |
| def json(self): | |
| """ | |
| Return a json representation | |
| """ | |
| result = "{\n" | |
| result += ' "Column names": ["A", "B", "C", "D", "E", "F", "G"],\n' | |
| for y in range(6): | |
| result += f' "Row {6-y}": [' | |
| for x in range(7): | |
| result += f'"{pieces[self.cells[5-y][x]]}", ' | |
| result = result[:-2] + "],\n" | |
| result = result[:-2] + "\n}" | |
| return result | |
| def alternative(self): | |
| """ | |
| An alternative representation, used in prompting so that the LLM sees this 2 ways | |
| """ | |
| result = " A B C D E F G\n" | |
| for y in range(6): | |
| for x in range(7): | |
| result += " " + simple[self.cells[5 - y][x]] | |
| result += "\n" | |
| return result | |
| def height(self, x: int) -> int: | |
| """ | |
| Return the height of the given column | |
| """ | |
| height = 0 | |
| while height < 6 and self.cells[height][x] != EMPTY: | |
| height += 1 | |
| return height | |
| def legal_moves(self) -> List[str]: | |
| """ | |
| Return the names of columns that are not full | |
| """ | |
| return [cols[x] for x in range(7) if self.height(x) < 6] | |
| def illegal_moves(self) -> List[str]: | |
| """ | |
| Return the names of columns that are full | |
| """ | |
| return [cols[x] for x in range(7) if self.height(x) == 6] | |
| def winning_line(self, x: int, y: int, dx: int, dy: int) -> int: | |
| """ | |
| Return RED or YELLOW if this cell is the start of a 4 in the row going in the direction dx, dy | |
| Or EMPTY if not | |
| """ | |
| color = self.cells[y][x] | |
| for pointer in range(1, 4): | |
| xp = x + dx * pointer | |
| yp = y + dy * pointer | |
| if not (0 <= xp <= 6 and 0 <= yp <= 5) or self.cells[yp][xp] != color: | |
| return EMPTY | |
| return color | |
| def winning_cell(self, x: int, y: int) -> int: | |
| """ | |
| Return RED or YELLOW if this cell is the start of a 4 in the row | |
| Or EMPTY if not | |
| For performance reasons, only look in 4 of the possible 8 directions, | |
| (because this test will run on both sides of the 4-in-a-row) | |
| """ | |
| for dx, dy in ((0, 1), (1, 1), (1, 0), (1, -1)): | |
| if winner := self.winning_line(x, y, dx, dy): | |
| return winner | |
| return EMPTY | |
| def wins(self) -> int: | |
| """ | |
| Return RED or YELLOW if there is a 4-in-a-row of that color on the board | |
| Or EMPTY if not | |
| """ | |
| for y in range(6): | |
| for x in range(7): | |
| if winner := self.winning_cell(x, y): | |
| return winner | |
| return EMPTY | |
| def move(self, x: int): | |
| """ | |
| Make a move in the given column | |
| """ | |
| y = self.height(x) | |
| self.cells[y][x] = self.player | |
| self.latest_x, self.latest_y = x, y | |
| if winner := self.wins(): | |
| self.winner = winner | |
| elif not self.legal_moves: | |
| self.draw = True | |
| else: | |
| self.player = -1 * self.player | |
| return self | |
| def is_active(self) -> bool: | |
| """ | |
| Return true if the game has not yet ended | |
| """ | |
| return not self.winner and not self.draw | |