Moonchess: vanishing tic-tac-toe, online and practice modes
Online play over WebSockets with any-string game codes, seats and spectators, and a 1-second heartbeat. Practice mode against Columbina, backed by a full solver, with an accuracy slider, spoilers, undo and piece freshness. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
c52f30dc59
18 files changed
+1743
No files matched your search
@@ -0,0 +1,111 @@
|
||||
"""Rules for vanishing tic-tac-toe.
|
||||
|
||||
Each seat may have at most MAX_PIECES on the board. With a full set, your oldest
|
||||
piece is removed as your turn starts, so you have at most MAX_PIECES - 1 pieces down
|
||||
while placing and may place on the square it left.
|
||||
"""
|
||||
|
||||
LINES = [
|
||||
(0, 1, 2), (3, 4, 5), (6, 7, 8), # rows
|
||||
(0, 3, 6), (1, 4, 7), (2, 5, 8), # columns
|
||||
(0, 4, 8), (2, 4, 6), # diagonals
|
||||
]
|
||||
MAX_PIECES = 3
|
||||
|
||||
|
||||
class Game:
|
||||
def __init__(self):
|
||||
self.first = 0 # seat that moves first this round; alternates on rematch
|
||||
self.new_round()
|
||||
|
||||
@classmethod
|
||||
def from_history(cls, history, turn):
|
||||
"""Rebuild a round from each seat's cells (oldest first). Raises ValueError if invalid."""
|
||||
try:
|
||||
hx, ho = (list(h) for h in history)
|
||||
except (TypeError, ValueError):
|
||||
raise ValueError("Invalid position")
|
||||
cells = hx + ho
|
||||
if (turn not in (0, 1)
|
||||
or any(type(c) is not int or not 0 <= c < 9 for c in cells)
|
||||
or len(set(cells)) != len(cells)
|
||||
or len(hx) > MAX_PIECES or len(ho) > MAX_PIECES):
|
||||
raise ValueError("Invalid position")
|
||||
game = cls()
|
||||
game.history = (hx, ho)
|
||||
game.turn = turn
|
||||
for seat, hist in enumerate(game.history):
|
||||
for c in hist:
|
||||
game.board[c] = seat
|
||||
for seat in (0, 1):
|
||||
line = game._winning_line(seat)
|
||||
if line:
|
||||
game.winner, game.win_line = seat, line
|
||||
if game.winner is None and len(game.history[turn]) == MAX_PIECES:
|
||||
raise ValueError("Invalid position") # the mover's oldest should already be gone
|
||||
return game
|
||||
|
||||
def new_round(self):
|
||||
self.board = [None] * 9 # None, 0 (X) or 1 (O)
|
||||
self.history = ([], []) # per seat, oldest cell first
|
||||
self.turn = self.first
|
||||
self.winner = None
|
||||
self.win_line = None
|
||||
self.rematch_votes = set()
|
||||
|
||||
def doomed(self):
|
||||
"""The waiting player's oldest piece, which vanishes when their turn starts."""
|
||||
if self.winner is not None:
|
||||
return None
|
||||
hist = self.history[1 - self.turn]
|
||||
return hist[0] if len(hist) == MAX_PIECES else None
|
||||
|
||||
def play(self, seat, cell):
|
||||
"""Place a piece. Returns an error string, or None on success."""
|
||||
if self.winner is not None:
|
||||
return "The round is over"
|
||||
if seat != self.turn:
|
||||
return "Not your turn"
|
||||
if not isinstance(cell, int) or not 0 <= cell < 9:
|
||||
return "Invalid square"
|
||||
if self.board[cell] is not None:
|
||||
return "That square is taken"
|
||||
|
||||
self.board[cell] = seat
|
||||
self.history[seat].append(cell)
|
||||
|
||||
line = self._winning_line(seat)
|
||||
if line:
|
||||
self.winner = seat
|
||||
self.win_line = line
|
||||
else:
|
||||
self.turn = 1 - seat
|
||||
hist = self.history[self.turn]
|
||||
if len(hist) == MAX_PIECES:
|
||||
self.board[hist.pop(0)] = None
|
||||
return None
|
||||
|
||||
def _winning_line(self, seat):
|
||||
for line in LINES:
|
||||
if all(self.board[i] == seat for i in line):
|
||||
return list(line)
|
||||
return None
|
||||
|
||||
def vote_rematch(self, seat):
|
||||
if self.winner is None:
|
||||
return
|
||||
self.rematch_votes.add(seat)
|
||||
if self.rematch_votes >= {0, 1}:
|
||||
self.first = 1 - self.first
|
||||
self.new_round()
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"board": self.board,
|
||||
"history": self.history,
|
||||
"turn": self.turn,
|
||||
"winner": self.winner,
|
||||
"winLine": self.win_line,
|
||||
"doomed": self.doomed(),
|
||||
"rematch": [0 in self.rematch_votes, 1 in self.rematch_votes],
|
||||
}
|
||||
Reference in new issue
Block a user