Skip to content

Commit

Permalink
Add threefold repetition rule
Browse files Browse the repository at this point in the history
  • Loading branch information
josephlou5 committed Jun 9, 2023
1 parent 1510ccf commit d98ebfe
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 34 deletions.
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## v2.2.0

- Added command-line parsing to allow launching a game between builtin players
- Added threefold repetition rule

## v2.1.0

Expand Down
44 changes: 27 additions & 17 deletions Documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,17 +111,18 @@ the end game state, and more.

**Properties**

| Name | Type | Description |
| ----------------- | ------------------------------ | ----------------------------------------------------------------------- |
| `id` | `int` | The game state id. |
| `white` | [`Player`][] | The white player. |
| `black` | [`Player`][] | The black player. |
| `prev` | `Optional[GameState]` | The previous `GameState`. |
| `move` | `Optional[`[`PieceMoved`][]`]` | The move from the previous `GameState`. |
| `current_color` | [`Color`][] | The color of the current player. |
| `current_player` | [`Player`][] | The current player. |
| `half_move_clock` | `int` | The number of half-moves since the last capture or the last pawn moved. |
| `num_moves` | `int` | The number of full moves played in the entire game. |
| Name | Type | Description |
| ----------------- | -------------------------------- | ----------------------------------------------------------------------- |
| `id` | `int` | The game state id. |
| `white` | [`Player`][] | The white player. |
| `black` | [`Player`][] | The black player. |
| `prev` | `Optional[GameState]` | The previous `GameState`. |
| `move` | `Optional[`[`PieceMoved`][]`]` | The move from the previous `GameState`. |
| `end_game_state` | `Optional[`[`EndGameState`][]`]` | The end game state. |
| `current_color` | [`Color`][] | The color of the current player. |
| `current_player` | [`Player`][] | The current player. |
| `half_move_clock` | `int` | The number of half-moves since the last capture or the last pawn moved. |
| `num_moves` | `int` | The number of full moves played in the entire game. |

**Methods**

Expand Down Expand Up @@ -185,13 +186,9 @@ the end game state, and more.

Returns whether the current player is in stalemate.

- `is_kings_draw() -> bool`

Returns whether the game is a draw by only having kings.

- `is_draw() -> bool`

Returns whether the game is a draw (by the fifty move rule).
Returns whether the game is a draw.

- `is_in_check() -> bool`

Expand Down Expand Up @@ -665,8 +662,21 @@ Enum for the possible states for the end of the game.

- `EndGameState.CHECKMATE`
- `EndGameState.STALEMATE`
- `EndGameState.ONLY_KINGS_DRAW`: A draw by only having kings left on the board.
- `EndGameState.INSUFFICIENT_MATERIAL_DRAW`: A draw by having insufficient
material to end in a checkmate (such as only two kings).
- `EndGameState.FIFTY_MOVE_DRAW`: A draw through the fifty move rule.
- `EndGameState.THREEFOLD_REPETITION_DRAW`: A draw through the threefold
repetition rule.

**Methods**

- `human_readable() -> str`

Returns a human-readable name of this state.

- `is_draw() -> bool`

Returns whether the current state is a type of draw.

### `HumanPlayer`

Expand Down
45 changes: 32 additions & 13 deletions src/alicechess/game_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# =============================================================================

import math
from collections import Counter
from itertools import count, zip_longest
from typing import Dict, Iterable, Iterator, Optional, Self, Type

Expand Down Expand Up @@ -48,6 +49,8 @@ class GameState: # pylint: disable=too-many-public-methods
move (Optional[PieceMoved]): The move from the previous
GameState, or None if this is the first state.
end_game_state (Optional[EndGameState]): The end game state.
current_color (Color): The color of the current player.
current_player (Player): The current player.
Expand All @@ -73,10 +76,8 @@ class GameState: # pylint: disable=too-many-public-methods
Returns the winner of the game (for checkmate).
is_in_stalemate() -> bool
Returns whether the current player is in stalemate.
is_kings_draw() -> bool
Returns whether the game is a draw by only having kings.
is_draw() -> bool
Returns whether the game is a draw (by the fifty move rule).
Returns whether the game is a draw.
is_in_check() -> bool
Returns whether the current player is in check.
Expand Down Expand Up @@ -161,6 +162,11 @@ def __init__(
self._half_move_clock = half_move_clock
self._num_moves = num_moves

if prev is None:
self._seen_positions = Counter()
else:
self._seen_positions = prev._seen_positions.copy()

self._captured = tuple(captured)

# construct board
Expand Down Expand Up @@ -284,23 +290,33 @@ def color_case(color, value):
_, r, c = en_passant_pawn.pos
r -= en_passant_pawn.dr
fen.append(Position(r, c).code)
# seen position (basically fen without the move counters)
board_position = tuple(fen)
self._seen_positions[board_position] += 1
# half move clock
fen.append(str(self._half_move_clock))
# full move number
fen.append(str(self._num_moves))
# combine
self._fen = " ".join(fen)

self._is_in_check = False
# some draws could technically happen at the same time, so this
# order is arbitrary
if len(self._board) == 2:
# only two kings are left
self._is_in_check = False
self._end_game_state = EndGameState.ONLY_KINGS_DRAW
# there could be more cases for this, but not sure what they
# are for alice chess
self._end_game_state = EndGameState.INSUFFICIENT_MATERIAL_DRAW
return
if self._half_move_clock >= 100:
# 50 move rule
self._is_in_check = False
self._end_game_state = EndGameState.FIFTY_MOVE_DRAW
return
if self._seen_positions[board_position] >= 3:
# threefold repetition rule
self._end_game_state = EndGameState.THREEFOLD_REPETITION_DRAW
return

# calculate all the possible moves
calculator = MovesCalculator(
Expand All @@ -323,12 +339,12 @@ def debug(self):
print("=" * 50)
print(self.fen())
print(self.board_to_str())
print(self.moves_to_str())
if self.is_game_over():
print("end game state:", self._end_game_state)
if self.is_in_checkmate():
print("winner:", self.winner())
else:
print(self.moves_to_str())
print("in check:", self.is_in_check())

@classmethod
Expand Down Expand Up @@ -579,6 +595,11 @@ def move(self) -> Optional[PieceMoved]:
"""
return self._move

@property
def end_game_state(self) -> Optional[EndGameState]:
"""The end game state."""
return self._end_game_state

@property
def current_color(self) -> Color:
"""The color of the current player."""
Expand Down Expand Up @@ -737,13 +758,11 @@ def is_in_stalemate(self) -> bool:
"""Returns whether the current player is in stalemate."""
return self._end_game_state is EndGameState.STALEMATE

def is_kings_draw(self) -> bool:
"""Returns whether the game is a draw by only having kings."""
return self._end_game_state is EndGameState.ONLY_KINGS_DRAW

def is_draw(self) -> bool:
"""Returns whether the game is a draw (by the fifty move rule)."""
return self._end_game_state is EndGameState.FIFTY_MOVE_DRAW
"""Returns whether the game is a draw."""
return (
self._end_game_state is not None and self._end_game_state.is_draw()
)

def is_in_check(self) -> bool:
"""Returns whether the current player is in check."""
Expand Down
15 changes: 14 additions & 1 deletion src/alicechess/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,21 @@ class EndGameState(_enum.Enum):

CHECKMATE = _enum.auto()
STALEMATE = _enum.auto()
ONLY_KINGS_DRAW = _enum.auto()
INSUFFICIENT_MATERIAL_DRAW = _enum.auto()
FIFTY_MOVE_DRAW = _enum.auto()
THREEFOLD_REPETITION_DRAW = _enum.auto()

def human_readable(self) -> str:
"""Returns a human-readable name of this state."""
return self.name.replace("_", " ").title()

def is_draw(self) -> bool:
"""Returns whether the current state is a type of draw."""
return self in (
self.INSUFFICIENT_MATERIAL_DRAW,
self.FIFTY_MOVE_DRAW,
self.THREEFOLD_REPETITION_DRAW,
)


# =============================================================================
Expand Down
4 changes: 1 addition & 3 deletions src/alicechess/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -735,10 +735,8 @@ def _end_turn(self):
text = f"Checkmate. {game.winner().title()} won!"
elif game.is_in_stalemate():
text = "Stalemate."
elif game.is_kings_draw():
text = "Draw."
elif game.is_draw():
text = "Draw (50 moves rule)."
text = f"Draw ({game.end_game_state.human_readable()})."
self._show(self._state_text, text=text)
return

Expand Down

0 comments on commit d98ebfe

Please sign in to comment.