Skip to content

Commit cf088b5

Browse files
committed
init Nard
0 parents  commit cf088b5

File tree

9 files changed

+625
-0
lines changed

9 files changed

+625
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.idea

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2023 Emil Humbatov
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Nard Game (Backgammon)
2+
3+
Classic board game of Nard (Backgammon) for two players.
4+
5+
#### Common rules
6+
- Players: the game is played by two players
7+
- Tables board of 24 points or spaces; 2 dice; 30 pieces (15 per player)
8+
- Both players throw a die to decide who plays first; the one with the higher die leads off
9+
- Bearing off:
10+
- Begins once all 15 pieces are in the home quadrant
11+
- One piece is removed from the point corresponding to the roll of each die
12+
- If there are no piece on the point corresponding to a die roll, the player must make a legal move with a piece further away
13+
- If that is not possible, a piece is borne off from the furthest point that is occupied.
14+
15+
## Usage
16+
17+
#### Playing
18+
```python
19+
from nard_backgammon import (
20+
Nard,
21+
NardState,
22+
Player,
23+
get_random_move,
24+
board_to_str)
25+
26+
27+
# If the first_player is None, then the starting
28+
# player is found on the first roll `first_roll()`
29+
game = Nard.new(first_roll_player=Player.WHITE)
30+
31+
while True:
32+
33+
match game.state:
34+
# If first_roll_player is None then
35+
# NardState is will be FIRST_ROLL at the init
36+
case NardState.FIRST_ROLL:
37+
game.first_roll()
38+
39+
case NardState.PLAYING:
40+
# get valid moves for active player
41+
moves = game.get_valid_moves()
42+
if not moves:
43+
game.skip()
44+
continue
45+
move = get_random_move(moves)
46+
game.play_move(move)
47+
print(board_to_str(game))
48+
49+
case NardState.ENDED:
50+
print(f'Winner: {game.outcome.winner}')
51+
break # End game, exit the loop
52+
```
53+
54+
#### Output
55+
```text
56+
...
57+
||12-11-10-9--8--7-||6--5--4--3--2--1-||
58+
||o o x x x || ||
59+
||2 2 7 2 || ||
60+
|| || || White off: 0
61+
|| || || Black off: 4
62+
|| || || Dices: (4, 3)
63+
|| 2 || 4 3 ||
64+
|| o o || o o o o ||
65+
||13-14-15-16-17-18||19-20-21-22-23-24||
66+
...
67+
Winner: Player.WHITE
68+
```

main.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from nard_backgammon import (
2+
Nard,
3+
NardState,
4+
Player,
5+
get_random_move,
6+
board_to_str)
7+
8+
9+
# If the first_player is None, then the starting
10+
# player is found on the first roll `first_roll()`
11+
game = Nard.new(first_roll_player=Player.WHITE)
12+
13+
while True:
14+
15+
match game.state:
16+
# If first_roll_player is None then
17+
# NardState is will be FIRST_ROLL at the init
18+
case NardState.FIRST_ROLL:
19+
game.first_roll()
20+
21+
case NardState.PLAYING:
22+
# get valid moves for active player
23+
moves = game.get_valid_moves()
24+
if not moves:
25+
game.skip()
26+
continue
27+
move = get_random_move(moves)
28+
game.play_move(move)
29+
print(board_to_str(game))
30+
31+
case NardState.ENDED:
32+
print(f'Winner: {game.outcome.winner}')
33+
break

nard_backgammon/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""
2+
:authors: cmdtorch
3+
:license: MIT, see LICENSE file
4+
5+
:copyright: (c) 2023 cmdtorch
6+
"""
7+
8+
version = '0.1.0'
9+
10+
from .nard import (Nard, NardState, NardOutcome, get_random_move, board_to_str)
11+
from .board import Board
12+
from .player import Player
13+
from .exceptions import NardError
14+
15+
__authors__ = 'cmdtorch'
16+
__version__ = version
17+
__email__ = 'emil4154515@gmail.com'

nard_backgammon/board.py

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
"""
2+
:authors: cmdtorch
3+
:license: MIT, see LICENSE file
4+
5+
:copyright: (c) 2023 cmdtorch
6+
"""
7+
8+
from dataclasses import dataclass
9+
from typing import Tuple, Optional, List
10+
from .player import Player
11+
from .exceptions import NardError
12+
13+
14+
@dataclass
15+
class Move:
16+
"""
17+
Move class for Nard game
18+
"""
19+
source: int
20+
direction: Optional[int] = None
21+
22+
def __repr__(self):
23+
return f'[from: {self.source}, to: {self.direction}'
24+
25+
26+
class Board:
27+
"""
28+
Board for Nard game
29+
"""
30+
31+
def __init__(self):
32+
self.slots: Tuple[int, ...] = self._generate_slots()
33+
self._white_off: int = 0
34+
self._black_off: int = 0
35+
36+
@property
37+
def white_off(self) -> int:
38+
"""
39+
Number of white checkers offed from the board
40+
:return: number of checkers
41+
"""
42+
return self._white_off
43+
44+
@property
45+
def black_off(self) -> int:
46+
"""
47+
Number of black checkers offed from the board
48+
:return: number of checkers
49+
"""
50+
return self._black_off
51+
52+
def add_move(self, player: Player, move: Move) -> Tuple[int, ...]:
53+
"""
54+
Apply move on the board
55+
:param player: the player who played
56+
:param move: move for apply
57+
:return: new state of slots
58+
"""
59+
slots = list(self.player_pov_slots(player))
60+
slots[move.source] = slots[move.source] + (1 if player == Player.BLACK else -1)
61+
if move.direction:
62+
slots[move.direction] = slots[move.direction] + (-1 if player == Player.BLACK else 1)
63+
else:
64+
if player == Player.WHITE:
65+
self._white_off += 1
66+
else:
67+
self._black_off += 1
68+
69+
self._set_new_slots(player, slots)
70+
return self.slots
71+
72+
def off(self, player: Player, source: int) -> None:
73+
"""
74+
Off checker from the board
75+
:param player: the player who played
76+
:param source: source of checker
77+
:return: None
78+
"""
79+
if not self.is_off_possible(player):
80+
raise NardError('There are checkers that are not at home')
81+
82+
slots = list(self.player_pov_slots(player))
83+
slots[source] = slots[source] + (1 if player == Player.BLACK else -1)
84+
if player == Player.WHITE:
85+
self._white_off += 1
86+
else:
87+
self._black_off += 1
88+
89+
self._set_new_slots(player, slots)
90+
# distance = 24 - source
91+
92+
def get_valid_moves(self, player: Player, dices: Tuple[int]) -> Optional[List[Move]]:
93+
"""
94+
Get list of valid moves.
95+
:param player: the player who played
96+
:param dices: dice number for move
97+
:return: list of valid moves
98+
"""
99+
100+
def player_piece_value():
101+
return 1 if player == Player.BLACK else -1
102+
103+
def is_unacceptable_mars(slots, source: int, direction: int) -> bool:
104+
slots_after_move = list(slots)
105+
slots_after_move[source] = slots_after_move[source] + player_piece_value()
106+
slots_after_move[direction] = slots_after_move[direction] + player_piece_value()
107+
slots_after_move_handler = [1 if s > 0 else -1 if s < 0 else 0
108+
for s in slots_after_move]
109+
opponent_value = -1 if Player.WHITE == player else 1
110+
111+
checkers_in_row = 0
112+
for i, s in enumerate(slots_after_move_handler):
113+
if player.WHITE and s > 0 or player.BLACK and s < 0:
114+
checkers_in_row += 1
115+
if checkers_in_row < 6:
116+
continue
117+
if i <= 12 and opponent_value not in slots_after_move_handler[0:13] or \
118+
i >= 13 and opponent_value not in slots_after_move_handler[14:24] and\
119+
-1 not in slots_after_move_handler[0:13]:
120+
return True
121+
else:
122+
checkers_in_row = 0
123+
124+
return False
125+
126+
player_view_slots = self.player_pov_slots(player)
127+
# CHECK FOR MOVE CHECKERS
128+
moves = []
129+
for index, slot in enumerate(player_view_slots):
130+
if not self.is_player_cell(player, slot):
131+
continue
132+
for dice in dices:
133+
try:
134+
direction = index + dice
135+
moved_slot = player_view_slots[direction]
136+
if moved_slot != 0 and not self.is_player_cell(player, moved_slot):
137+
continue
138+
if not is_unacceptable_mars(player_view_slots, index, direction):
139+
moves.append(Move(index, direction))
140+
except IndexError:
141+
pass
142+
143+
# CHECK FOR OFF CHECKERS
144+
if not self.is_off_possible(player):
145+
return moves
146+
147+
off_moves = []
148+
home = list(player_view_slots[18:24])
149+
for index, slot in enumerate(home):
150+
if not self.is_player_cell(player, slot):
151+
continue
152+
153+
for dice in dices:
154+
if 6 - index == dice:
155+
off_moves.append(Move(index + 18, None))
156+
elif not moves:
157+
off_moves.append(Move(index + 18, None))
158+
159+
if off_moves:
160+
return off_moves
161+
return moves
162+
163+
def is_off_possible(self, player: Player) -> bool:
164+
"""
165+
Checking whether a player can off his checkers
166+
:param player: the player for check
167+
:return: `True` or `False`
168+
"""
169+
slots_exclude_home = list(self.player_pov_slots(player))[0:18]
170+
match player:
171+
case Player.WHITE:
172+
if [cell for cell in slots_exclude_home if cell > 0]:
173+
return False
174+
case Player.BLACK:
175+
if [cell for cell in slots_exclude_home if cell < 0]:
176+
return False
177+
return True
178+
179+
def player_pov_slots(self, player: Player) -> Tuple[int, ...]:
180+
"""
181+
Slots by the player point of view
182+
:param player: the player for pov
183+
:return: slots list
184+
"""
185+
if player == Player.BLACK:
186+
slots = list(self.slots)
187+
first_half = slots[0:12]
188+
return tuple(slots[12:24] + first_half)
189+
return self.slots
190+
191+
def _set_new_slots(self, player: Player, new_slots: List[int]) -> None:
192+
if player == Player.BLACK:
193+
first_half = new_slots[0:12]
194+
new_slots = tuple(new_slots[12:24] + first_half)
195+
self.slots = tuple(new_slots)
196+
197+
@staticmethod
198+
def is_player_cell(player: Player, slot: int) -> bool:
199+
"""
200+
Check is has player piece in slot
201+
:param player: player for piece
202+
:param slot: slot number
203+
:return: `True` or `False`
204+
"""
205+
if slot == 0:
206+
return False
207+
208+
if player == Player.BLACK and slot < 0 \
209+
or player == Player.WHITE and slot > 0:
210+
return True
211+
return False
212+
213+
@staticmethod
214+
def _generate_slots():
215+
slots = tuple(15 if i == 0 else
216+
-15 if i == 12 else 0 for i in range(24))
217+
return slots
218+
219+
def __repr__(self):
220+
return f'Slots: {self.slots} |' \
221+
f' White checkers offed: {self.white_off} |' \
222+
f' Black checkers offed: {self.black_off}'
223+
224+
225+
def get_distance(move: Move) -> int:
226+
"""
227+
Delta of move
228+
:param move: move
229+
:return: delta
230+
"""
231+
if move.direction > move.source:
232+
return move.direction - move.source
233+
return (24 - move.source) + move.direction

nard_backgammon/exceptions.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""
2+
:authors: cmdtorch
3+
:license: MIT, see LICENSE file
4+
5+
:copyright: (c) 2023 cmdtorch
6+
"""
7+
8+
9+
class NardError(Exception):
10+
"""
11+
Nard Exceptions
12+
"""

0 commit comments

Comments
 (0)