connect / arena /board.py
ed-donner's picture
Improved board representation, misc cleanup
d2c1e33
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