Skip to content

Commit

Permalink
Merge pull request #19 from nickzuber/feature/play-as-black
Browse files Browse the repository at this point in the history
  • Loading branch information
nickzuber authored May 31, 2021
2 parents 9770f0c + ff9385d commit cbecba7
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 44 deletions.
35 changes: 24 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
<img src="https://user-images.githubusercontent.com/10540865/119232802-80c34700-baf4-11eb-9fed-af558575ae4e.png" />

### Table of Contents
- [Installation](#installation)
- [Pip](#pip)
- [Arch Linux](#arch-linux)
- [Usage](#usage)
- [How to start playing](#how-to-start-playing)
- [How to play](#how-to-play)
- [License](#license)

- [Installation](#installation)
- [Pip](#pip)
- [Arch Linux](#arch-linux)
- [Usage](#usage)
- [How to start playing](#how-to-start-playing)
- [How to play](#how-to-play)
- [License](#license)

## Installation

Expand Down Expand Up @@ -43,19 +44,31 @@ To play against the default level 1 (easiest) version of the Stockfish engine, j
$ chs
```

To see all possible options, use the help command.

```
$ chs help
```

To play as the black pieces, use the `--play-black` flag.

```
$ chs --play-black
```

You can also specify the level of the engine if you want to tweak the difficulty.

```
$ chs level=8
$ chs --level=8
```

### How to play

There are a few things you can do while playing:

* Make moves using valid algebraic notation (e.g. `Nf3`, `e4`, etc.).
* Take back your last move by playing `back` instead of a valid move.
* Get a hint from the engine by playing `hint` instead of a valid move.
- Make moves using valid algebraic notation (e.g. `Nf3`, `e4`, etc.).
- Take back your last move by playing `back` instead of a valid move.
- Get a hint from the engine by playing `hint` instead of a valid move.

## License

Expand Down
30 changes: 25 additions & 5 deletions chs/__main__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env python

import chess
import sys
import os
from chs.utils.core import Colors, Levels
Expand Down Expand Up @@ -29,13 +30,31 @@ def is_version_command(arg):
arg == '-v'
)

def get_level_from_args(args):
lvl = [arg for arg in args if "--level" in arg]
if lvl:
try:
num = int(lvl[0].split('=')[1])
return Levels.level_of_int(num)
except:
return Levels.ONE
return Levels.ONE

def get_player_from_args(args):
player = [arg for arg in args if "--play-black" in arg]
if player:
return chess.BLACK
return chess.WHITE

def main():
if len(sys.argv) > 1 and is_help_command(sys.argv[1]):
print('Usage: chs [COMMAND]\n')
print('Usage: chs [COMMAND] [FLAGS]\n')
print('Valid values for [COMMAND]')
print(' help Print all the possible usage information')
print(' version Print the current version')
print(' level=[LVL] Start a game with the given difficulty level')
print('\nValid values for [FLAGS]')
print(' --play-black Play the game with the black pieces')
print(' --level=[LVL] Start a game with the given difficulty level')
print('\nValid values for [LVL]')
print(' 1 The least difficult setting')
print(' 2..7 Increasing difficulty')
Expand All @@ -51,11 +70,12 @@ def main():
print('Running chs {}v{}{}\n'.format(Colors.BOLD, get_version(), Colors.RESET))
else:
try:
num = int(sys.argv[1].split('=')[1])
level = Levels.level_of_int(num)
level = get_level_from_args(sys.argv)
play_as = get_player_from_args(sys.argv)
except:
level = Levels.ONE
client = Client(level)
play_as = chess.WHITE
client = Client(level, play_as)
client.run()

def run():
Expand Down
25 changes: 19 additions & 6 deletions chs/client/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ class Client(object):
BACK = 'back'
HINT = 'hint'

def __init__(self, level):
self.ui_board = Board(level)
def __init__(self, level, play_as):
self.ui_board = Board(level, play_as)
self.play_as = play_as
self.board = chess.Board()
self.parser = FenParser(self.board.fen())
self.engine = Engine(level) # Engine you're playing against.
Expand All @@ -42,8 +43,7 @@ def run(self):
try:
while True:
self.check_game_over()
to_move = self.parser.get_to_move(self.fen())
if to_move == 'w':
if self.is_user_move():
self.make_turn()
else:
self.computer_turn()
Expand Down Expand Up @@ -112,7 +112,10 @@ def make_turn(self, meta=(False, None)):
self.board.help_engine_hint = self.board.uci(hint.move)
else:
s = self.board.parse_san(move)
self.board.san_move_stack_white.append(self.board.san(s))
if self.play_as == chess.WHITE:
self.board.san_move_stack_white.append(self.board.san(s))
else:
self.board.san_move_stack_black.append(self.board.san(s))
self.board.push_san(move)
self.board.help_engine_hint = None # Reset hint if you've made your move.
except ValueError:
Expand All @@ -130,8 +133,18 @@ def computer_turn(self):
Styles.PADDING_SMALL, Styles.PADDING_SMALL, Colors.RESET, Colors.GRAY, Colors.RESET)
)
result = self.engine.play(self.board)
self.board.san_move_stack_black.append(self.board.san(result.move))
if self.play_as == chess.WHITE:
self.board.san_move_stack_black.append(self.board.san(result.move))
else:
self.board.san_move_stack_white.append(self.board.san(result.move))
self.board.push(result.move)

def fen(self):
return self.board.fen()

def is_user_move(self):
to_move = self.parser.get_to_move(self.fen())
if self.play_as == chess.WHITE:
return to_move == 'w'
else:
return to_move == 'b'
11 changes: 6 additions & 5 deletions chs/engine/stockfish.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
engine_path = 'stockfish_10_x64_windows.exe'
elif 'Linux' in platform.system():
engine_path = 'stockfish_10_x64_linux'
else:
engine_path = 'stockfish_10_x64_mac'
else:
engine_path = 'stockfish_13_x64_mac'

class Engine(object):
def __init__(self, level):
Expand All @@ -22,19 +22,20 @@ def __init__(self, level):
def play(self, board, time=1.500):
return self.engine.play(board, chess.engine.Limit(time=time))

def score(self, board):
def score(self, board, pov=chess.WHITE):
try:
info = self.engine.analyse(board, chess.engine.Limit(time=0.500))
cp = chess.engine.PovScore(info['score'], chess.WHITE).pov(chess.WHITE).relative.score()
cp = chess.engine.PovScore(info['score'], pov).pov(pov).relative.score()
return cp
except chess.engine.EngineTerminatedError:
return None

def normalize(self, cp):
if cp is None:
return None
# https://github.com/ornicar/lila/blob/80646821b238d044aed5baf9efb7201cd4793b8b/ui/ceval/src/winningChances.ts#L10
raw_score = 2 / (1 + math.exp(-0.004 * cp)) - 1
return round(raw_score, 1)
return round(raw_score, 3)

def done(self):
try:
Expand Down
Binary file added chs/engine/stockfish_13_x64_mac
Binary file not shown.
76 changes: 60 additions & 16 deletions chs/ui/board.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@ def safe_pop(l):
except IndexError:
return None

def round_to_nearest(x, base=25):
return base * round(x / base)

class Board(object):
def __init__(self, level):
def __init__(self, level, play_as):
self._play_as = play_as
self._level = level
self._score = 0
self._cp = 0
Expand Down Expand Up @@ -83,9 +87,13 @@ def _generate(self, fen, board, game_over, loading=False):
hint_positions = None

# Draw the board and pieces
positions = fen.split(' ')[0]
fen_positions = fen.split(' ')[0]

# If user is black, reverse the positions so we draw black first
positions = self.white_or_black(fen_positions, fen_positions[::-1])
ranks = positions.split('/')
rank_i = 8
rank_i = self.white_or_black(8, 1)
rank_i_meta = 8

def get_piece_composed(piece):
if turn == 'b':
Expand All @@ -94,21 +102,25 @@ def get_piece_composed(piece):
return self.get_piece_colored(piece, False, is_check)

for rank in ranks:
file_i = 1
file_i = self.white_or_black(1, 8)
file_i_meta = 1
pieces = flatten(map(get_piece_composed, list(rank)))
ui_board += '{}{}{} '.format(Styles.PADDING_MEDIUM, Colors.GRAY, str(rank_i))
# Add each piece + tile
for piece in pieces:
color = self.get_tile_color_from_position(rank_i, file_i, position_changes, hint_positions)
ui_board += '{}{}'.format(color, piece)
file_i = file_i + 1
file_i = self.white_or_black(file_i + 1, file_i - 1)
file_i_meta = file_i_meta + 1
# Finish the rank
ui_board += '{} {}{}\n'.format(Colors.RESET, self.get_bar_section(rank_i), self.get_meta_section(board, fen, rank_i, game_over))
rank_i = rank_i - 1
ui_board += '{} {}{}\n'.format(Colors.RESET, self.get_bar_section(rank_i_meta), self.get_meta_section(board, fen, rank_i_meta, game_over))
rank_i = self.white_or_black(rank_i - 1, rank_i + 1)
rank_i_meta = rank_i_meta - 1

# Add files label
# Add files label - If user is black, reverse the file numbering since board is flipped
ui_board += ' {}{}'.format(Styles.PADDING_MEDIUM, Colors.GRAY)
for f in self.FILES:
files_ui = self.white_or_black(self.FILES, self.FILES[::-1])
for f in files_ui:
ui_board += ' {}'.format(f)
# Extra meta text
ui_board += '{}{}\n{}'.format(' ' * 6, self.get_meta_section(board, fen, 0, game_over), Colors.RESET)
Expand All @@ -128,9 +140,15 @@ def get_meta_section(self, board, fen, rank, game_over):
# Calculate advantage pieces
(captured_white, captured_black) = self._get_captured_pieces(positions)
(white_advantage, black_advantage) = self._diff_pieces(captured_white, captured_black)
advantage_text = ''.join(map(self.get_piece, list(white_advantage)))
advantage_text = ''.join(map(
self.get_piece,
list(self.white_or_black(white_advantage, black_advantage))
))
# Calculate advantage score
diff_score = self._score_pieces(white_advantage) - self._score_pieces(black_advantage)
diff_score = self.white_or_black(
self._score_pieces(white_advantage) - self._score_pieces(black_advantage),
self._score_pieces(black_advantage) - self._score_pieces(white_advantage)
)
score_text = '+{}'.format(diff_score) if diff_score > 0 else ''
return '{}{}{}{}'.format(padding, Colors.DULL_GRAY, advantage_text, score_text)
if rank == 1:
Expand All @@ -140,7 +158,10 @@ def get_meta_section(self, board, fen, rank, game_over):
text = '{}{}'.format(Colors.ORANGE, self.string_of_game_over(game_over))
return '{}{}'.format(padding, text)
else:
return '{}{}{} cp:{}'.format(padding, Colors.DULL_GRAY, str(self._score).ljust(11), self._cp)
normalized_score = round(self._score * 100, 1)
prefix = '+' if normalized_score >= 0 else ''
winning_potential = '{}{}'.format(prefix, normalized_score)
return '{}{}{} cp:{}'.format(padding, Colors.DULL_GRAY, winning_potential.ljust(11), self._cp)
if rank == 3:
return '{}{}┗━━━━━━━━━━━━━━━━━━━┛'.format(padding_alt, Colors.DULL_GRAY)
if rank == 4:
Expand Down Expand Up @@ -172,9 +193,15 @@ def get_meta_section(self, board, fen, rank, game_over):
# Calculate advantage pieces
(captured_white, captured_black) = self._get_captured_pieces(positions)
(white_advantage, black_advantage) = self._diff_pieces(captured_white, captured_black)
advantage_text = ''.join(map(self.get_piece, list(black_advantage)))
advantage_text = ''.join(map(
self.get_piece,
list(self.white_or_black(black_advantage, white_advantage))
))
# Calculate advantage score
diff_score = self._score_pieces(black_advantage) - self._score_pieces(white_advantage)
diff_score = self.white_or_black(
self._score_pieces(black_advantage) - self._score_pieces(white_advantage),
self._score_pieces(white_advantage) - self._score_pieces(black_advantage)
)
score_text = '+{}'.format(diff_score) if diff_score > 0 else ''
return '{}{}{}{}'.format(padding, Colors.DULL_GRAY, advantage_text, score_text)
if rank == 8:
Expand All @@ -190,11 +217,22 @@ def get_bar_section(self, rank):
percentage = ''
tick = ' '
color = Colors.DULL_GRAY
normalized_score = self._score + 100
normalized_score = self.white_or_black(
round_to_nearest((self._score * 100) + 100),
200 - (round_to_nearest((self._score * 100) + 100))
)
block_range = rank * 25

# Color the bar blocks
if normalized_score >= block_range:
color = Colors.GREEN if self._score >= 0 else Colors.RED
if normalized_score == 100:
color = Colors.GREEN if self._score >= 0 else Colors.RED
else:
pos_color = self.white_or_black(Colors.GREEN, Colors.RED)
neg_color = self.white_or_black(Colors.RED, Colors.GREEN)
color = pos_color if self._score >= 0 else neg_color

# Include the tick if we're in the center.
if block_range == 125:
tick = '{}_{}'.format(Colors.DULL_GRAY, color)
return '{}{}█ {}{}'.format(color, tick, percentage, Colors.RESET)
Expand Down Expand Up @@ -344,6 +382,12 @@ def string_of_game_over(self, game_over):
return 'White resigns 0-1'
return 'Game over'

def is_user_white(self):
return self._play_as == chess.WHITE

def white_or_black(self, a, b):
return a if self.is_user_white() else b

def clear(self):
if os.name == 'nt': # For windows
os.system('cls')
Expand Down
3 changes: 2 additions & 1 deletion tests/test_setup.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import os
import subprocess
import chess

from chs.ui.board import Board
from tests.framework.base_command_test_case import BaseCommandTestCase


ui = Board(1)
ui = Board(1, chess.WHITE)

class TestInit(BaseCommandTestCase):
def test_no_captured_pieces(self):
Expand Down

0 comments on commit cbecba7

Please sign in to comment.