Skip to content

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:

["1.", "d4", "d5", "2.", "c4", "Nf6", ..., "Qe7#", "1-0"]

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

def get_n_moves(game: str) -> int:
    return len(parse_moves(game))

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 in piece_map → named piece
  • move[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

01

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.

02

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.

03

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.