S3Q1 · Chess Game Analysis¶
⚡ Quick Reference
Six functions on a chess game string in Standard Algebraic Notation.
piece_map = {'K':'King','Q':'Queen','R':'Rook','B':'Bishop','N':'Knight'}
piece_values = {"Pawn":1,"Bishop":2,"Knight":3,"Rook":4,"Queen":5,"King":6}
def parse_moves(game):
return [t for t in game.split()
if not t.endswith('.') and t not in ('1-0','0-1','1/2-1/2')]
def get_n_moves(game):
return len(parse_moves(game))
def get_piece(move):
if move.startswith('O'): return ['King','Rook']
return [piece_map.get(move[0], 'Pawn')]
def count_piece_moves(moves):
counts = {p:0 for p in piece_map.values()}
counts['Pawn'] = 0
for m in moves:
for p in get_piece(m): counts[p] += 1
return counts
def most_used_piece(game, player):
moves = parse_moves(game)
player_moves = moves[0::2] if player == 'white' else moves[1::2]
counts = count_piece_moves(player_moves)
return max(counts, key=lambda p: (counts[p], piece_values[p]))
def remaining_pieces(game, player):
moves = parse_moves(game)
# opponent captures reduce player's pieces
opp_moves = moves[1::2] if player == 'white' else moves[0::2]
captures = sum(1 for m in opp_moves if 'x' in m)
return 16 - captures
def n_checks(game, player):
moves = parse_moves(game)
player_moves = moves[0::2] if player == 'white' else moves[1::2]
return sum(1 for m in player_moves if m.endswith('+'))
Key rules:
- White moves are at even indices (0,2,4…), black at odd indices (1,3,5…)
- Castling counts as a King move AND a Rook move
- x in a move = capture (reduces opponent's piece count)
- + at end = check; count in the player's own moves
- Each player starts with 16 pieces
Problem Statement¶
Problem
Implement six functions to analyse a chess game string in Standard Algebraic Notation.
Sample game:
game = "1. d4 d5 2. c4 Nf6 3. cxd5 Nxd5 4. Nf3 Be6 5. e4 Nb6 6. Nc3 f5 \
7. Ng5 Qd7 8. Nb5 c6 9. Nxe6 Qxe6 10. Nc7+ Kd8 11. Nxe6+ Ke8 \
12. Nc7+ Kd8 13. Bf4 N8d7 14. d5 e6 15. dxe6 Bb4+ 16. Ke2 Rc8 \
17. exd7 Nxd7 18. Ne6+ Ke7 19. Nxg7 Rcg8 20. Bd6+ Bxd6 21. Nxf5+ Ke6 \
22. Qxd6+ Kf7 23. Qxd7+ Kf8 24. Qe7# 1-0"
Understanding the token structure¶
After game.split(), tokens look like:
Three types to skip:
- Move numbers: "1.", "2.", … -end with "."
- Result tokens: "1-0", "0-1", "1/2-1/2"
Everything else is a move. White and black alternate:
Index: 0 1 2 3 4 5 6 7 ...
Move: d4 d5 c4 Nf6 cxd5 Nxd5 Nf3 Be6
↑ ↑ ↑ ↑ → white (even indices)
↑ ↑ ↑ ↑ → black (odd indices)
Function 1 -parse_moves¶
def parse_moves(game: str) -> list:
return [t for t in game.split()
if not t.endswith('.') and t not in ('1-0', '0-1', '1/2-1/2')]
Returns: ['d4', 'd5', 'c4', 'Nf6', ...] -47 moves total.
Function 2 -get_n_moves¶
Helper -get_piece(move)¶
Used by functions 3 and 4. Returns a list of piece names for a single move:
def get_piece(move: str) -> list:
if move.startswith('O'): # castling O-O or O-O-O
return ['King', 'Rook']
return [piece_map.get(move[0], 'Pawn')]
move[0]is uppercase and inpiece_map→ named piecemove[0]is lowercase (file letter) → Pawn- Starts with
'O'→ castling → both King and Rook
Function 3 -count_piece_moves¶
Takes the parsed move list (not the game string):
def count_piece_moves(moves: list) -> dict:
counts = {p: 0 for p in ['King','Queen','Rook','Bishop','Knight','Pawn']}
for m in moves:
for piece in get_piece(m):
counts[piece] += 1
return counts
Function 4 -most_used_piece¶
Split moves by player, count pieces, find maximum. Ties broken by piece value:
def most_used_piece(game: str, player: str) -> str:
moves = parse_moves(game)
player_moves = moves[0::2] if player == 'white' else moves[1::2]
counts = count_piece_moves(player_moves)
return max(counts, key=lambda p: (counts[p], piece_values[p]))
max with a tuple key (count, value) -higher count wins, ties broken by higher piece value.
Function 5 -remaining_pieces¶
Each player starts with 16 pieces. The opponent's captures reduce a player's count. White captures happen in white's moves (even indices); they reduce black's count. Black captures (odd indices) reduce white's count:
def remaining_pieces(game: str, player: str) -> int:
moves = parse_moves(game)
# opponent's moves contain the captures that reduce player's pieces
opp_moves = moves[1::2] if player == 'white' else moves[0::2]
captures = sum(1 for m in opp_moves if 'x' in m)
return 16 - captures
Function 6 -n_checks¶
A check is a + at the end of the move. Count + endings in the player's own moves:
def n_checks(game: str, player: str) -> int:
moves = parse_moves(game)
player_moves = moves[0::2] if player == 'white' else moves[1::2]
return sum(1 for m in player_moves if m.endswith('+'))
Why endswith('+') not 'in'?
'x' in move checks for captures anywhere. But '+' can appear inside some annotations. Using endswith('+') ensures we only count actual check symbols at the end of the move token.
Complete solution approaches¶
piece_map = {'K':'King','Q':'Queen','R':'Rook','B':'Bishop','N':'Knight'}
piece_values = {"Pawn":1,"Bishop":2,"Knight":3,"Rook":4,"Queen":5,"King":6}
def parse_moves(game: str) -> list:
return [t for t in game.split()
if not t.endswith('.') and t not in ('1-0','0-1','1/2-1/2')]
def get_n_moves(game: str) -> int:
return len(parse_moves(game))
def get_piece(move: str) -> list:
if move.startswith('O'):
return ['King', 'Rook']
return [piece_map.get(move[0], 'Pawn')]
def count_piece_moves(moves: list) -> dict:
counts = {p: 0 for p in ['King','Queen','Rook','Bishop','Knight','Pawn']}
for m in moves:
for p in get_piece(m):
counts[p] += 1
return counts
def most_used_piece(game: str, player: str) -> str:
moves = parse_moves(game)
player_moves = moves[0::2] if player == 'white' else moves[1::2]
counts = count_piece_moves(player_moves)
return max(counts, key=lambda p: (counts[p], piece_values[p]))
def remaining_pieces(game: str, player: str) -> int:
moves = parse_moves(game)
opp_moves = moves[1::2] if player == 'white' else moves[0::2]
return 16 - sum(1 for m in opp_moves if 'x' in m)
def n_checks(game: str, player: str) -> int:
moves = parse_moves(game)
player_moves = moves[0::2] if player == 'white' else moves[1::2]
return sum(1 for m in player_moves if m.endswith('+'))
piece_map = {'K':'King','Q':'Queen','R':'Rook','B':'Bishop','N':'Knight'}
piece_values = {"Pawn":1,"Bishop":2,"Knight":3,"Rook":4,"Queen":5,"King":6}
def parse_moves(game: str) -> list:
return list(filter(
lambda t: not t.endswith('.') and t not in ('1-0','0-1','1/2-1/2'),
game.split()
))
def get_n_moves(game: str) -> int:
return len(parse_moves(game))
def get_piece(move: str) -> list:
if move.startswith('O'):
return ['King', 'Rook']
return [piece_map.get(move[0], 'Pawn')]
def count_piece_moves(moves: list) -> dict:
counts = {p: 0 for p in ['King','Queen','Rook','Bishop','Knight','Pawn']}
for pieces in map(get_piece, moves):
for p in pieces:
counts[p] += 1
return counts
def most_used_piece(game: str, player: str) -> str:
moves = parse_moves(game)
player_moves = moves[0::2] if player == 'white' else moves[1::2]
counts = count_piece_moves(player_moves)
return max(counts, key=lambda p: (counts[p], piece_values[p]))
def remaining_pieces(game: str, player: str) -> int:
moves = parse_moves(game)
opp_moves = moves[1::2] if player == 'white' else moves[0::2]
return 16 - len(list(filter(lambda m: 'x' in m, opp_moves)))
def n_checks(game: str, player: str) -> int:
moves = parse_moves(game)
player_moves = moves[0::2] if player == 'white' else moves[1::2]
return len(list(filter(lambda m: m.endswith('+'), player_moves)))
Key takeaways¶
moves[0::2] and moves[1::2]
Step slicing splits the move list into white moves (even indices) and black moves (odd indices). This is the cleanest way to separate players without a loop counter.
Tie-breaking with tuple key in max()
max(counts, key=lambda p: (counts[p], piece_values[p])) -tuples compare lexicographically. Higher count wins; on a tie, the higher-value piece wins. One max() call handles both criteria.
Opponent's captures reduce your pieces
To find white's remaining pieces, count captures in black's moves (odd indices). It's easy to get this backwards -the player being analysed loses pieces to the opponent's captures, not their own.