Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #13 allow short algebraic notation moves. #14

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Chessnut/board.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,10 @@ def find_piece(self, symbol):
piece is not on the board.
"""
return ''.join(self._position).find(symbol)

def find_all_pieces(self, symbol):
"""
Find all indexes of the specified piece on the board, returns an empty list
if the piece is not on the board.
"""
return [indx for indx, piece in enumerate(self._position) if piece == symbol]
81 changes: 81 additions & 0 deletions Chessnut/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
chess rules.
"""

import re
from collections import namedtuple

from Chessnut.board import Board
Expand Down Expand Up @@ -205,6 +206,13 @@ def apply_move(self, move):
# state update must happen after castling
self.set_fen(' '.join(str(x) for x in [self.board] + list(fields)))

def apply_san_move(self, move):
"""
Take a move in algebraic notation, e.g. Rac1, Nf3, Nbd7 and
make the move.
"""
self.apply_move(self.san_to_long(move))

def get_moves(self, player=None, idx_list=range(64)):
"""
Get a list containing the legal moves for pieces owned by the
Expand Down Expand Up @@ -366,3 +374,76 @@ def status(self):
status = Game.STALEMATE

return status

def san_to_long(self, san_str, verbose=False):
san = San(san_str)
if not san.end_square:
# Castling move, end square depends on player
if san.castling == 'K' and self.state.player == 'w':
san.end_square = 'g1'
san.from_hints = 'e1'
elif san.castling == 'Q' and self.state.player == 'w':
san.end_square = 'c1'
san.from_hints = 'e1'
elif san.castling == 'K' and self.state.player == 'b':
san.end_square = 'g8'
san.from_hints = 'e8'
elif san.castling == 'Q' and self.state.player == 'b':
san.end_square = 'c8'
san.from_hints = 'e8'

if len(san.from_hints) == 2:
return san.from_hints + san.end_square

if not san.piece:
san.piece = 'P'
if self.state.player == 'b':
san.piece = san.piece.lower()

start_squares = self.board.find_all_pieces(san.piece)
if len(start_squares) == 1:
return Game.i2xy(start_squares[0]) + san.end_square + san.promotion.lower()

# If there's more than one piece on the board of this type,
# we have to determine which ones have a legal move
rays = MOVES.get(san.piece, [''] * 64)

for square in start_squares:
if san.from_hints not in Game.i2xy(square):
continue

for ray in rays[square]:
moves = self._trace_ray(square, san.piece, ray, self.state.player)
for move in moves:
if san.end_square == move[2:4] and (not san.promotion or san.promotion.lower() == move[-1]):
# We found our move, we can just return it
return move


common_san_re = re.compile(r'([KQBNR]?)([a-h]?[1-8]?)x?([a-h][1-8])=?([QBNR])?\+{0,2}')


class San(object):
"""
Represents Short algebraic notation, eg. Bf5, Rc1, Nbd7
"""
piece = ''
from_hints = ''
end_square = ''
promotion = ''
castling = ''

def __init__(self, san):
self.san = san
common_move = common_san_re.match(self.san)

if common_move:
self.piece = common_move.group(1) or ''
self.from_hints = common_move.group(2) or ''
self.end_square = common_move.group(3)
self.promotion = common_move.group(4) or ''

elif self.san.replace('-', '') in ('00', 'OO'):
self.castling = 'K'
elif self.san.replace('-', '') in ('000', 'OOO'):
self.castling = 'Q'
6 changes: 6 additions & 0 deletions Chessnut/tests/test_board.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,9 @@ def test_move_piece(self):
self.board.move_piece(52, 36, 'P') # e2e4
self.assertEqual(str(self.board),
'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR')

def test_find_all_pieces(self):
self.board.set_position('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR')
self.assertEqual(self.board.find_all_pieces('P'), [48, 49, 50, 51, 52, 53, 54, 55])
self.assertEqual(self.board.find_all_pieces('R'), [56, 63])
self.assertEqual(self.board.find_all_pieces('r'), [0, 7])
147 changes: 145 additions & 2 deletions Chessnut/tests/test_game.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

import unittest
from Chessnut.game import Game, InvalidMove
from Chessnut.game import Game, InvalidMove, San


# Default FEN string
Expand All @@ -22,7 +22,7 @@ def setUp(self):
self.game = Game()

def test_i2xy(self):
for idx in xrange(64):
for idx in range(64):
self.assertIn(Game.i2xy(idx), ALG_POS)

def test_xy2i(self):
Expand Down Expand Up @@ -165,6 +165,69 @@ def test_apply_move(self):
with self.assertRaises(InvalidMove):
self.game.apply_move('e2e2')

def test_apply_san_move(self):
# pawn promotion
fen = '3qk1b1/P7/8/8/8/8/7P/4K3 w - - 0 1'
self.game = Game(fen=fen)
self.game.apply_san_move('a8=Q')
self.assertEqual(str(self.game), 'Q2qk1b1/8/8/8/8/8/7P/4K3 b - - 0 1')

# apply moves
self.game.apply_san_move('Bh7')
self.assertEqual(str(self.game), 'Q2qk3/7b/8/8/8/8/7P/4K3 w - - 1 2')
self.game.apply_san_move('h4')
self.assertEqual(str(self.game), 'Q2qk3/7b/8/8/7P/8/8/4K3 b - h3 0 2')

# white castling
fen = 'r3k2r/pppqbppp/3pb3/8/8/3PB3/PPPQBPPP/R3K2R w KQkq - 0 7'
new_fen = 'r3k2r/pppqbppp/3pb3/8/8/3PB3/PPPQBPPP/R4RK1 b kq - 1 7'
self.game = Game(fen=fen)
self.game.apply_san_move('0-0')
self.assertEqual(str(self.game), new_fen)
new_fen = 'r3k2r/pppqbppp/3pb3/8/8/3PB3/PPPQBPPP/2KR3R b kq - 1 7'
self.game = Game(fen=fen)
self.game.apply_san_move('0-0-0')
self.assertEqual(str(self.game), new_fen)

# black castling
fen = 'r3k2r/pppqbppp/3pb3/8/8/3PB3/PPPQBPPP/R3K2R b KQkq - 0 7'
new_fen = 'r4rk1/pppqbppp/3pb3/8/8/3PB3/PPPQBPPP/R3K2R w KQ - 1 8'
self.game = Game(fen=fen)
self.game.apply_san_move('0-0')
self.assertEqual(str(self.game), new_fen)
new_fen = '2kr3r/pppqbppp/3pb3/8/8/3PB3/PPPQBPPP/R3K2R w KQ - 1 8'
self.game = Game(fen=fen)
self.game.apply_san_move('0-0-0')
self.assertEqual(str(self.game), new_fen)

# Disable castling on capture
fen = '1r2k2r/3nb1Qp/p1pp4/3p4/3P4/P1N2P2/1PP3PP/R1B3K1 w k - 0 22'
new_fen = '1r2k2Q/3nb2p/p1pp4/3p4/3P4/P1N2P2/1PP3PP/R1B3K1 b - - 0 22'
self.game = Game(fen=fen)
self.game.apply_san_move('Qxh8')
self.assertEqual(str(self.game), new_fen)

# white en passant
fen = 'rnbqkbnr/ppp2ppp/4p3/3pP3/8/8/PPPP1PPP/RNBQKBNR w KQkq d6 0 1'
self.game = Game(fen=fen)
self.game.apply_san_move('exd6')
new_fen = 'rnbqkbnr/ppp2ppp/3Pp3/8/8/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1'
self.assertEqual(str(self.game), new_fen)

# black en passant
fen = 'rnbqkbnr/ppp1pppp/8/8/3pP3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1'
self.game = Game(fen=fen)
self.game.apply_san_move('dxe3')
new_fen = 'rnbqkbnr/ppp1pppp/8/8/8/4p3/PPPP1PPP/RNBQKBNR w KQkq - 0 2'
self.assertEqual(str(self.game), new_fen)

# capture
fen = 'r2q1rk1/pppbbppp/2n5/3p4/3PN3/4PN2/1PPBBPPP/R2Q1RK1 b - - 0 9'
self.game = Game(fen=fen)
self.game.apply_san_move('dxe4')
n_fen = 'r2q1rk1/pppbbppp/2n5/8/3Pp3/4PN2/1PPBBPPP/R2Q1RK1 w - - 0 10'
self.assertEqual(str(self.game), n_fen)

def test_status(self):

# NORMAL
Expand All @@ -182,3 +245,83 @@ def test_status(self):
# STALEMATE
game = Game(fen='8/8/8/8/8/7k/5q2/7K w - - 0 37')
self.assertEqual(game.status, Game.STALEMATE)

def test_san_to_long(self):
self.game.reset() # reset board to starting position
game = [
'e4', 'e5',
'Nf3', 'd6',
'd4', 'Bg4',
'dxe5', 'Bxf3',
'Qxf3', 'dxe5',
'Bc4', 'Nf6',
'Qb3', 'Qe7',
'Nc3', 'c6',
'Bg5', 'b5',
'Nxb5', 'cxb5',
'Bxb5', 'Nbd7',
'0-0-0', 'Rd8',
'Rxd7', 'Rxd7',
'Rd1', 'Qe6',
'Bxd7', 'Nxd7',
'Qb8+', 'Nxb8',
'Rd8++'
]
for move in game:
self.game.apply_san_move(move)
self.assertEqual(self.game.get_fen(), '1n1Rkb1r/p4ppp/4q3/4p1B1/4P3/8/PPP2PPP/2K5 b k - 1 17')

self.game = Game(fen='8/P7/1K6/8/k7/8/8/8 w - - 0 40')
self.game.apply_san_move('a8=Q')
self.assertEqual(self.game.get_fen(), 'Q7/8/1K6/8/k7/8/8/8 b - - 0 40')

self.game = Game(fen='8/8/1K6/8/k7/8/p7/8 b - - 0 40')
self.game.apply_san_move('a1=Q')
self.assertEqual(self.game.get_fen(), '8/8/1K6/8/k7/8/8/q7 w - - 0 41')


class SanTest(unittest.TestCase):
def test_simple_move(self):
san = San('Rc1')
self.assertEqual(san.piece, 'R')
self.assertEqual(san.end_square, 'c1')

def test_from_hints(self):
san = San('Rac1')
self.assertEqual(san.piece, 'R')
self.assertEqual(san.end_square, 'c1')
self.assertEqual(san.from_hints, 'a')

san = San('R1c1')
self.assertEqual(san.piece, 'R')
self.assertEqual(san.end_square, 'c1')
self.assertEqual(san.from_hints, '1')

san = San('Rac1')
self.assertEqual(san.piece, 'R')
self.assertEqual(san.end_square, 'c1')
self.assertEqual(san.from_hints, 'a')

san = San('Ra1c1')
self.assertEqual(san.piece, 'R')
self.assertEqual(san.end_square, 'c1')
self.assertEqual(san.from_hints, 'a1')

def test_promotion(self):
san = San('e8=Q')
self.assertEqual(san.piece, '')
self.assertEqual(san.end_square, 'e8')
self.assertEqual(san.promotion, 'Q')

def test_castling(self):
san = San('0-0-0')
self.assertEqual(san.castling, 'Q')

san = San('O-O-O')
self.assertEqual(san.castling, 'Q')

san = San('0-0')
self.assertEqual(san.castling, 'K')

san = San('O-O')
self.assertEqual(san.castling, 'K')
2 changes: 1 addition & 1 deletion Chessnut/tests/test_moves.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def test_moves(self):
self.assertIn(piece, MOVES)

# test that every starting position is in the dictionary
for idx in xrange(64):
for idx in range(64):
self.assertIsNotNone(MOVES[piece][idx])

# test ordering of moves in each ray (should radiate out
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
coverage==3.7
coverage==4.3.4