Skip to content

Add FEN Support and Debug Panel for Testing #77

@walshb421

Description

@walshb421

Summary

Add FEN (Forsyth-Edwards Notation) support to enable loading arbitrary board positions. This is critical infrastructure for testing new features without playing through complete games, and provides a debugging interface for development.

What is FEN?

FEN is a standard notation for describing chess positions in a single line string.

Example: rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1

Format: [piece placement] [active color] [castling rights] [en passant] [halfmove] [fullmove]

  • Piece placement: 8 ranks separated by /, lowercase = black, uppercase = white
  • Active color: w or b
  • Castling rights: KQkq (King/Queenside for white/black) or - if none
  • En passant: Target square (e.g., e3) or - if none
  • Halfmove clock: Moves since last pawn move or capture (for 50-move rule)
  • Fullmove number: Increments after black's move

Implementation Requirements

Backend (Python)

1. Add load_fen(fen_string) method to ChessBoard

def load_fen(self, fen_string):
    """
    Parse FEN string and initialize board state.

    Args:
        fen_string: Valid FEN notation string

    Returns:
        Game state JSON (same as game_to_json)

    Raises:
        ValueError: If FEN string is invalid
    """

Must handle:

  • Parse piece placement and create appropriate Piece objects
  • Set self.turn based on active color
  • Set castling flags (white_king_moved, white_rook_a_moved, etc.)
  • Store en passant target square for pawn validation
  • Clear and rebuild self.moves history (or mark as loaded position)

2. Add to_fen() method to ChessBoard

def to_fen(self):
    """
    Export current board state as FEN string.

    Returns:
        str: Valid FEN notation of current position
    """

3. Add WebSocket callbacks

self.add_callback("load_fen", lambda obj: self.load_fen(obj['fen']))
self.add_callback("get_fixtures", lambda obj: self.get_fixtures())

4. Create test fixtures module

Create engine/fixtures.py:

POSITIONS = {
    # Basic positions
    "starting": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
    "empty": "8/8/8/8/8/8/8/8 w - - 0 1",

    # Checkmate positions
    "fools_mate": "rnb1kbnr/pppp1ppp/4p3/8/6Pq/5P2/PPPPP2P/RNBQKBNR w KQkq - 1 3",
    "scholars_mate": "r1bqkb1r/pppp1Qpp/2n2n2/4p3/2B1P3/8/PPPP1PPP/RNB1K1NR b KQkq - 0 4",
    "back_rank_mate": "6k1/5ppp/8/8/8/8/8/R3K3 b Q - 0 1",

    # Special moves
    "en_passant_white": "rnbqkbnr/ppp1p1pp/8/3pPp2/8/8/PPPP1PPP/RNBQKBNR w KQkq d6 0 3",
    "en_passant_black": "rnbqkbnr/pppp1ppp/8/8/3Pp3/8/PPP1PPPP/RNBQKBNR b KQkq d3 0 3",
    "castling_available": "r3k2r/pppppppp/8/8/8/8/PPPPPPPP/R3K2R w KQkq - 0 1",
    "castling_kingside_only": "r3k2r/pppppppp/8/8/8/8/PPPPPPPP/R3K2R w Kk - 0 1",

    # Check scenarios
    "white_in_check": "rnbqkbnr/ppppp1pp/8/5p1Q/4P3/8/PPPP1PPP/RNB1KBNR b KQkq - 1 2",
    "double_check": "rnbqkb1r/pppp1ppp/5n2/4N3/2B1P3/8/PPPP1PPP/RNBQK2R b KQkq - 0 1",
    "pinned_piece": "rnb1kbnr/pppp1ppp/8/4p3/7q/5NP1/PPPPP2P/RNBQKB1R w KQkq - 0 1",

    # Pawn promotion
    "promotion_ready_white": "8/P7/8/8/8/8/8/4K2k w - - 0 1",
    "promotion_ready_black": "4K2k/8/8/8/8/8/p7/8 b - - 0 1",

    # Stalemate
    "stalemate": "k7/8/1K6/8/8/8/8/8 b - - 0 1",
}

Frontend (Vue.js)

5. Update chess.js composable

Add methods for FEN operations:

const loadFen = (fen) => {
  send(JSON.stringify({ load_fen: { fen } }));
};

// Export in useChess()
return {
  // ... existing exports
  loadFen,
};

6. Create DebugPanel.vue component

Replace or enhance existing Debug.vue with a debugging interface:

Features:

  • Text input for pasting FEN strings with "Load" button
  • "Copy Current FEN" button to export current position
  • Dropdown/select menu with categorized test fixtures:
    • Checkmates (Fool's Mate, Scholar's Mate, Back Rank)
    • Special Moves (En Passant, Castling scenarios)
    • Check Scenarios (In Check, Pinned Piece, Double Check)
    • Endgames (Stalemate, Promotion Ready)
  • Display current game state (turn, move count)
  • Reset button

7. Add toggle for debug panel

In App.vue:

  • Add keyboard shortcut (press D key) to show/hide debug panel
  • Or add a gear/debug icon button
  • Only render in development mode using import.meta.env.DEV

Testing

Unit Tests (Python)

Create tests/test_fen.py:

import pytest
from board import ChessBoard
from fixtures import POSITIONS

class TestFenParsing:
    def test_load_starting_position(self):
        board = ChessBoard()
        board.load_fen(POSITIONS["starting"])
        assert board.board[0][0].type == "Rook"
        assert board.board[0][0].color == "dark"
        assert board.board[7][4].type == "King"
        assert board.board[7][4].color == "light"
        assert board.turn == 0  # White to move

    def test_roundtrip_fen(self):
        board = ChessBoard()
        original_fen = POSITIONS["starting"]
        board.load_fen(original_fen)
        exported_fen = board.to_fen()
        assert exported_fen == original_fen

    def test_castling_rights_parsed(self):
        board = ChessBoard()
        board.load_fen(POSITIONS["castling_kingside_only"])
        assert board.white_king_moved == False
        assert board.white_rook_h_moved == False
        assert board.white_rook_a_moved == True  # No Q in FEN

    def test_en_passant_square_parsed(self):
        board = ChessBoard()
        board.load_fen(POSITIONS["en_passant_white"])
        # Verify en passant capture is valid
        result = board.can_move_piece("E5", "D6")
        assert result['valid'] == True
        assert result['special'] == 'en_passant'

    def test_invalid_fen_raises_error(self):
        board = ChessBoard()
        with pytest.raises(ValueError):
            board.load_fen("invalid fen string")

Integration Tests

class TestFixtureScenarios:
    def test_fools_mate_is_checkmate(self):
        board = ChessBoard()
        board.load_fen(POSITIONS["fools_mate"])
        # Once check detection is implemented:
        # assert board.is_checkmate('light') == True

    def test_stalemate_position(self):
        board = ChessBoard()
        board.load_fen(POSITIONS["stalemate"])
        # Once stalemate detection is implemented:
        # assert board.is_stalemate('dark') == True

Acceptance Criteria

  • load_fen() correctly parses all 6 FEN fields
  • to_fen() produces valid FEN that can be re-loaded
  • Castling rights are correctly set from FEN
  • En passant target square is correctly parsed and usable
  • WebSocket load_fen message loads position in UI
  • Debug panel allows loading FEN strings
  • Debug panel has dropdown for common test fixtures
  • Debug panel is only visible in development mode
  • All unit tests pass
  • Existing game functionality still works after loading FEN

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions