refactor: Consolidate repository structure - flatten from workspace pattern

Restructured project from nested workspace pattern to flat single-repo layout.
This eliminates redundant nesting and consolidates all project files under version control.

## Migration Summary

**Before:**
```
alex/ (workspace, not versioned)
├── chess-game/ (git repo)
│   ├── js/, css/, tests/
│   └── index.html
└── docs/ (planning, not versioned)
```

**After:**
```
alex/ (git repo, everything versioned)
├── js/, css/, tests/
├── index.html
├── docs/ (project documentation)
├── planning/ (historical planning docs)
├── .gitea/ (CI/CD)
└── CLAUDE.md (configuration)
```

## Changes Made

### Structure Consolidation
- Moved all chess-game/ contents to root level
- Removed redundant chess-game/ subdirectory
- Flattened directory structure (eliminated one nesting level)

### Documentation Organization
- Moved chess-game/docs/ → docs/ (project documentation)
- Moved alex/docs/ → planning/ (historical planning documents)
- Added CLAUDE.md (workspace configuration)
- Added IMPLEMENTATION_PROMPT.md (original project prompt)

### Version Control Improvements
- All project files now under version control
- Planning documents preserved in planning/ folder
- Merged .gitignore files (workspace + project)
- Added .claude/ agent configurations

### File Updates
- Updated .gitignore to include both workspace and project excludes
- Moved README.md to root level
- All import paths remain functional (relative paths unchanged)

## Benefits

 **Simpler Structure** - One level of nesting removed
 **Complete Versioning** - All documentation now in git
 **Standard Layout** - Matches open-source project conventions
 **Easier Navigation** - Direct access to all project files
 **CI/CD Compatible** - All workflows still functional

## Technical Validation

-  Node.js environment verified
-  Dependencies installed successfully
-  Dev server starts and responds
-  All core files present and accessible
-  Git repository functional

## Files Preserved

**Implementation Files:**
- js/ (3,517 lines of code)
- css/ (4 stylesheets)
- tests/ (87 test cases)
- index.html
- package.json

**CI/CD Pipeline:**
- .gitea/workflows/ci.yml
- .gitea/workflows/release.yml

**Documentation:**
- docs/ (12+ documentation files)
- planning/ (historical planning materials)
- README.md

**Configuration:**
- jest.config.js, babel.config.cjs, playwright.config.js
- .gitignore (merged)
- CLAUDE.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Christoph Wagner
2025-11-23 10:05:26 +01:00
co-authored by Claude
parent 1fd28d10b4
commit 5ad0700b41
326 changed files with 107368 additions and 281 deletions
@@ -0,0 +1,465 @@
/**
* Complete CSS Example - Chess Board Styling
* Use this as a reference for styling the chess game
*/
/* ==================== GLOBAL STYLES ==================== */
:root {
/* Board dimensions */
--board-size: 600px;
--square-size: calc(var(--board-size) / 8);
/* Colors */
--light-square: #f0d9b5;
--dark-square: #b58863;
--highlight-selected: rgba(255, 255, 0, 0.4);
--highlight-valid-move: rgba(0, 255, 0, 0.3);
--highlight-check: rgba(255, 0, 0, 0.5);
--highlight-last-move: rgba(255, 255, 0, 0.2);
/* Piece colors */
--piece-white: #ffffff;
--piece-black: #000000;
/* UI colors */
--bg-primary: #2c3e50;
--bg-secondary: #34495e;
--text-primary: #ecf0f1;
--text-secondary: #bdc3c7;
--accent: #3498db;
/* Spacing */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
/* Animations */
--transition-fast: 0.15s;
--transition-medium: 0.3s;
--transition-slow: 0.5s;
}
/* ==================== LAYOUT ==================== */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: var(--spacing-lg);
}
.game-container {
display: grid;
grid-template-columns: auto 1fr auto;
gap: var(--spacing-xl);
max-width: 1400px;
}
/* ==================== CHESS BOARD ==================== */
.board-container {
position: relative;
}
.board {
display: grid;
grid-template-columns: repeat(8, var(--square-size));
grid-template-rows: repeat(8, var(--square-size));
border: 2px solid var(--text-secondary);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
/* ==================== SQUARES ==================== */
.square {
width: var(--square-size);
height: var(--square-size);
display: flex;
align-items: center;
justify-content: center;
position: relative;
transition: background-color var(--transition-fast);
cursor: pointer;
}
/* Alternating colors */
.square--light {
background-color: var(--light-square);
}
.square--dark {
background-color: var(--dark-square);
}
/* Square states */
.square--selected {
background-color: var(--highlight-selected) !important;
box-shadow: inset 0 0 0 3px rgba(255, 255, 0, 0.8);
}
.square--valid-move {
cursor: pointer;
}
.square--valid-move::after {
content: '';
position: absolute;
width: 30%;
height: 30%;
background-color: var(--highlight-valid-move);
border-radius: 50%;
pointer-events: none;
}
/* Valid move on occupied square (capture) */
.square--valid-move.square--occupied::after {
width: 90%;
height: 90%;
background-color: transparent;
border: 3px solid rgba(255, 0, 0, 0.5);
border-radius: 50%;
}
.square--check {
background-color: var(--highlight-check) !important;
animation: pulse-check 1s infinite;
}
.square--last-move {
background-color: var(--highlight-last-move);
}
/* Hover effects */
.square:hover {
filter: brightness(1.1);
}
/* ==================== COORDINATES ==================== */
.coordinates {
position: absolute;
font-size: 12px;
font-weight: bold;
color: var(--text-secondary);
user-select: none;
}
.coordinate--file {
bottom: 2px;
right: 4px;
}
.coordinate--rank {
top: 2px;
left: 4px;
}
/* ==================== PIECES ==================== */
.piece {
font-size: calc(var(--square-size) * 0.7);
cursor: grab;
user-select: none;
transition: transform var(--transition-fast);
pointer-events: auto;
}
.piece:hover {
transform: scale(1.1);
}
.piece:active {
cursor: grabbing;
}
.piece--dragging {
opacity: 0.5;
cursor: grabbing;
}
/* Piece colors using filters (if using images) */
.piece--white {
filter: brightness(1.2);
}
.piece--black {
filter: brightness(0.3);
}
/* ==================== GAME INFO ==================== */
.game-info {
background: var(--bg-secondary);
padding: var(--spacing-lg);
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.game-info h1 {
font-size: 28px;
margin-bottom: var(--spacing-md);
color: var(--accent);
}
.turn-indicator {
padding: var(--spacing-md);
background: var(--bg-primary);
border-radius: 4px;
text-align: center;
margin-bottom: var(--spacing-md);
font-weight: bold;
font-size: 18px;
}
.turn-indicator--white {
color: #fff;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.turn-indicator--black {
color: #fff;
background: linear-gradient(135deg, #434343 0%, #000000 100%);
}
.status-message {
padding: var(--spacing-sm);
border-radius: 4px;
text-align: center;
margin-top: var(--spacing-md);
}
.status-message--info {
background: rgba(52, 152, 219, 0.2);
border: 1px solid var(--accent);
}
.status-message--warning {
background: rgba(230, 126, 34, 0.2);
border: 1px solid #e67e22;
color: #e67e22;
}
.status-message--error {
background: rgba(231, 76, 60, 0.2);
border: 1px solid #e74c3c;
color: #e74c3c;
}
.status-message--success {
background: rgba(46, 204, 113, 0.2);
border: 1px solid #2ecc71;
color: #2ecc71;
}
/* ==================== CONTROLS ==================== */
.game-controls {
background: var(--bg-secondary);
padding: var(--spacing-lg);
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
button {
padding: var(--spacing-md);
background: var(--accent);
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all var(--transition-fast);
}
button:hover {
background: #2980b9;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
button:active {
transform: translateY(0);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* ==================== CAPTURED PIECES ==================== */
.captured-pieces {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.captured-section {
background: var(--bg-primary);
padding: var(--spacing-md);
border-radius: 4px;
}
.captured-section h3 {
font-size: 14px;
margin-bottom: var(--spacing-sm);
color: var(--text-secondary);
}
.captured-list {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-xs);
}
.captured-piece {
font-size: 24px;
opacity: 0.6;
}
/* ==================== ANIMATIONS ==================== */
@keyframes pulse-check {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
@keyframes piece-move {
from {
transform: scale(1);
}
50% {
transform: scale(1.2);
}
to {
transform: scale(1);
}
}
@keyframes piece-capture {
from {
transform: scale(1) rotate(0deg);
opacity: 1;
}
to {
transform: scale(0) rotate(180deg);
opacity: 0;
}
}
.piece--moving {
animation: piece-move var(--transition-medium) ease;
}
.piece--captured {
animation: piece-capture var(--transition-medium) ease forwards;
}
/* ==================== RESPONSIVE DESIGN ==================== */
@media (max-width: 1200px) {
.game-container {
grid-template-columns: 1fr;
grid-template-rows: auto auto auto;
}
:root {
--board-size: 480px;
}
}
@media (max-width: 600px) {
:root {
--board-size: 320px;
}
body {
padding: var(--spacing-sm);
}
.piece {
font-size: calc(var(--square-size) * 0.6);
}
}
/* ==================== MODAL (for pawn promotion) ==================== */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal {
background: var(--bg-secondary);
padding: var(--spacing-xl);
border-radius: 8px;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4);
}
.modal h2 {
margin-bottom: var(--spacing-lg);
text-align: center;
}
.promotion-choices {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--spacing-md);
}
.promotion-choice {
padding: var(--spacing-lg);
font-size: 48px;
background: var(--bg-primary);
border: 2px solid transparent;
cursor: pointer;
transition: all var(--transition-fast);
}
.promotion-choice:hover {
background: var(--accent);
border-color: white;
transform: scale(1.1);
}
/* ==================== DRAG AND DROP ==================== */
.square--drag-over {
background-color: var(--highlight-valid-move) !important;
}
.no-select {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
@@ -0,0 +1,184 @@
/**
* @file knight-king-pattern.js
* @description Pattern for implementing non-sliding pieces (Knight, King)
* These pieces jump to specific positions
*/
import Piece from '../models/Piece.js';
import { DIRECTIONS } from '../utils/Constants.js';
import { isValidPosition } from '../utils/Helpers.js';
/**
* @class Knight
* @extends Piece
* @description Knight implementation using jump pattern
*
* Knight moves in L-shape: 2 squares in one direction, 1 square perpendicular
* Can jump over other pieces
*/
class Knight extends Piece {
constructor(color, position) {
super(color, position, 'knight');
}
/**
* Gets all valid moves for this knight
*
* @param {Board} board - Current board state
* @returns {Array<Object>} Array of valid positions
*/
getValidMoves(board) {
const moves = [];
const { row, col } = this.position;
// Knight has 8 possible L-shaped moves
const knightMoves = DIRECTIONS.KNIGHT;
// Check each possible move
for (const move of knightMoves) {
const newPos = {
row: row + move.row,
col: col + move.col
};
// Check if position is valid (on board)
if (!isValidPosition(newPos.row, newPos.col)) {
continue;
}
const piece = board.getPieceAt(newPos);
// Can move to empty square or capture enemy
if (!piece || piece.color !== this.color) {
moves.push(newPos);
}
}
return moves;
}
clone() {
const clone = new Knight(this.color, { ...this.position });
clone.hasMoved = this.hasMoved;
return clone;
}
}
/**
* @class King
* @extends Piece
* @description King implementation using adjacent square pattern
*
* King moves one square in any direction
* Special move: Castling (handled separately)
*/
class King extends Piece {
constructor(color, position) {
super(color, position, 'king');
}
/**
* Gets all valid moves for this king
*
* @param {Board} board - Current board state
* @returns {Array<Object>} Array of valid positions
*/
getValidMoves(board) {
const moves = [];
const { row, col } = this.position;
// King can move one square in 8 directions
const directions = [...DIRECTIONS.ORTHOGONAL, ...DIRECTIONS.DIAGONAL];
// Check each adjacent square
for (const direction of directions) {
const newPos = {
row: row + direction.row,
col: col + direction.col
};
// Check if position is valid
if (!isValidPosition(newPos.row, newPos.col)) {
continue;
}
const piece = board.getPieceAt(newPos);
// Can move to empty square or capture enemy
if (!piece || piece.color !== this.color) {
moves.push(newPos);
}
}
// TODO: Add castling moves
// Only if king hasn't moved
// Only if rook hasn't moved
// Only if squares between are empty
// Only if king is not in check
// Only if king doesn't pass through check
// See castling example in special-moves.js
return moves;
}
/**
* Checks if king can castle kingside
*
* @param {Board} board - Current board state
* @returns {boolean} True if kingside castling is legal
*/
canCastleKingside(board) {
// TODO: Implement castling validation
// This is complex and often handled in RuleEngine
return false;
}
/**
* Checks if king can castle queenside
*
* @param {Board} board - Current board state
* @returns {boolean} True if queenside castling is legal
*/
canCastleQueenside(board) {
// TODO: Implement castling validation
return false;
}
clone() {
const clone = new King(this.color, { ...this.position });
clone.hasMoved = this.hasMoved;
return clone;
}
}
/**
* PATTERN SUMMARY - NON-SLIDING PIECES:
*
* Unlike sliding pieces, these pieces:
* 1. Have a fixed set of possible moves (no sliding)
* 2. Can't be blocked (Knight) or move only 1 square (King)
* 3. Check each possible position directly
*
* Algorithm:
* 1. Get all possible move offsets
* 2. For each offset:
* a. Calculate new position
* b. Validate position is on board
* c. Check if square is empty or has enemy
* d. Add to moves if valid
*
* DIFFERENCE FROM SLIDING:
* - Sliding: Loop until blocked
* - Non-sliding: Check each position once
*
* KNIGHT SPECIAL:
* - Only piece that can jump over others
* - Don't need to check path, only destination
*
* KING SPECIAL:
* - Must not move into check (validated elsewhere)
* - Castling is complex special move
* - Usually limited to 8 moves, but critical to protect
*/
export { Knight, King };
@@ -0,0 +1,323 @@
/**
* @file move-validation-flow.js
* @description Complete example of move validation workflow
* Shows how all components work together
*/
/**
* MOVE VALIDATION FLOW
* ====================
*
* When a player attempts to move a piece, the system must validate
* the move through multiple levels:
*
* LEVEL 1: Piece Movement Rules
* - Can the piece move to that square according to its movement pattern?
* - Example: Can a knight move from e4 to f6?
*
* LEVEL 2: Path Obstruction
* - For sliding pieces, is the path clear?
* - Knights skip this check (they jump)
*
* LEVEL 3: Capture Validation
* - If capturing, is there an enemy piece at destination?
* - Can't capture own pieces
*
* LEVEL 4: King Safety
* - Does this move expose our king to check?
* - This is the most complex validation
*
* LEVEL 5: Special Rules
* - Castling requirements
* - En passant validity
* - Pawn promotion
*/
import MoveValidator from '../engine/MoveValidator.js';
import CheckDetector from '../engine/CheckDetector.js';
/**
* Example validation workflow
*/
class MoveValidationExample {
constructor(board, gameState) {
this.board = board;
this.gameState = gameState;
this.validator = new MoveValidator();
this.checkDetector = new CheckDetector();
}
/**
* Complete move validation example
*
* @param {Piece} piece - Piece to move
* @param {Object} from - Start position {row, col}
* @param {Object} to - End position {row, col}
* @returns {Object} Validation result with details
*/
validateMove(piece, from, to) {
const result = {
valid: false,
reason: '',
details: {}
};
// LEVEL 1: Basic piece movement
const validMoves = piece.getValidMoves(this.board);
const isPieceMove = validMoves.some(move =>
move.row === to.row && move.col === to.col
);
if (!isPieceMove) {
result.reason = 'Invalid move for this piece type';
result.details.validMoves = validMoves;
return result;
}
// LEVEL 2: Path obstruction (for sliding pieces)
if (this._isSlidingPiece(piece)) {
if (!this._isPathClear(from, to)) {
result.reason = 'Path is blocked';
return result;
}
}
// LEVEL 3: Capture validation
const targetPiece = this.board.getPieceAt(to);
if (targetPiece) {
if (targetPiece.color === piece.color) {
result.reason = 'Cannot capture own piece';
return result;
}
result.details.capture = targetPiece;
}
// LEVEL 4: King safety check
// This is the critical check - would this move expose king?
if (this._wouldExposeKing(piece, from, to)) {
result.reason = 'Move would expose king to check';
return result;
}
// LEVEL 5: Special moves validation
if (piece.type === 'king' && this._isCastlingMove(from, to)) {
if (!this._validateCastling(piece, from, to)) {
result.reason = 'Invalid castling';
return result;
}
result.details.castling = true;
}
if (piece.type === 'pawn' && this._isEnPassant(from, to)) {
if (!this._validateEnPassant(piece, from, to)) {
result.reason = 'Invalid en passant';
return result;
}
result.details.enPassant = true;
}
// All checks passed!
result.valid = true;
result.reason = 'Move is legal';
return result;
}
/**
* The critical check: Does this move expose our king?
*
* Algorithm:
* 1. Clone the board
* 2. Execute the move on the clone
* 3. Check if our king is in check on the cloned board
* 4. If yes, move is illegal
*
* @private
*/
_wouldExposeKing(piece, from, to) {
// Clone board to test move
const testBoard = this.board.clone();
// Execute move on test board
testBoard.movePiece(from, to);
// Check if our king is in check after this move
const ourColor = piece.color;
const isInCheck = this.checkDetector.isKingInCheck(ourColor, testBoard);
return isInCheck;
}
/**
* Check if path is clear for sliding pieces
*
* @private
*/
_isPathClear(from, to) {
// Get all squares between from and to
const path = this._getPathBetween(from, to);
// Check if any square is occupied
return path.every(pos => !this.board.getPieceAt(pos));
}
/**
* Validate castling move
*
* Requirements:
* 1. King hasn't moved
* 2. Rook hasn't moved
* 3. Squares between are empty
* 4. King is not in check
* 5. King doesn't pass through check
* 6. King doesn't end in check
*
* @private
*/
_validateCastling(king, from, to) {
// Check 1: King hasn't moved
if (king.hasMoved) {
return false;
}
// Check 2: Not currently in check
if (this.checkDetector.isKingInCheck(king.color, this.board)) {
return false;
}
// Determine kingside or queenside
const isKingside = to.col > from.col;
const rookCol = isKingside ? 7 : 0;
const rook = this.board.getPieceAt({ row: from.row, col: rookCol });
// Check 3: Rook exists and hasn't moved
if (!rook || rook.type !== 'rook' || rook.hasMoved) {
return false;
}
// Check 4: Squares between are empty
const path = this._getPathBetween(from, to);
if (!path.every(pos => !this.board.getPieceAt(pos))) {
return false;
}
// Check 5: King doesn't pass through check
for (const pos of path) {
const testBoard = this.board.clone();
testBoard.movePiece(from, pos);
if (this.checkDetector.isKingInCheck(king.color, testBoard)) {
return false;
}
}
return true;
}
/**
* Validate en passant capture
*
* Requirements:
* 1. Target square is the en passant square from game state
* 2. Enemy pawn is in correct position
* 3. Enemy pawn just moved two squares
*
* @private
*/
_validateEnPassant(pawn, from, to) {
const enPassantTarget = this.gameState.enPassantTarget;
if (!enPassantTarget) {
return false;
}
// Check if target matches en passant square
if (to.row !== enPassantTarget.row || to.col !== enPassantTarget.col) {
return false;
}
// Verify enemy pawn is in position
const enemyPawnRow = pawn.color === 'white' ? to.row + 1 : to.row - 1;
const enemyPawn = this.board.getPieceAt({ row: enemyPawnRow, col: to.col });
if (!enemyPawn || enemyPawn.type !== 'pawn' || enemyPawn.color === pawn.color) {
return false;
}
return true;
}
// Helper methods
_isSlidingPiece(piece) {
return ['rook', 'bishop', 'queen'].includes(piece.type);
}
_isCastlingMove(from, to) {
return Math.abs(to.col - from.col) === 2;
}
_isEnPassant(from, to) {
// En passant is diagonal move to empty square
return Math.abs(to.col - from.col) === 1 && !this.board.getPieceAt(to);
}
_getPathBetween(from, to) {
// Returns array of positions between from and to (exclusive)
// Implementation omitted for brevity
return [];
}
}
/**
* USAGE EXAMPLE
* =============
*/
// Setup
const board = new Board();
const gameState = new GameState();
const validator = new MoveValidationExample(board, gameState);
// Attempt to move a piece
const pawn = board.getPieceAt({ row: 6, col: 4 });
const from = { row: 6, col: 4 };
const to = { row: 4, col: 4 };
// Validate
const result = validator.validateMove(pawn, from, to);
if (result.valid) {
console.log('Move is legal:', result.reason);
if (result.details.capture) {
console.log('Captures:', result.details.capture);
}
// Execute the move
} else {
console.log('Move is illegal:', result.reason);
// Show error to user
}
/**
* COMMON PITFALLS
* ===============
*
* 1. INFINITE RECURSION
* - Don't call isKingInCheck inside getValidMoves
* - Use two-pass validation: basic moves → filter exposing king
*
* 2. FORGETTING TO CLONE
* - Always clone board before testing moves
* - Modifying original board breaks game state
*
* 3. MOVE ORDER DEPENDENCY
* - Some checks must come before others
* - King safety check must be last (most expensive)
*
* 4. EN PASSANT STATE
* - Must be cleared after any move that isn't en passant capture
* - Only valid immediately after opponent's two-square pawn move
*
* 5. CASTLING EDGE CASES
* - Check ALL conditions
* - Most common bug: forgetting to check if king passes through check
*/
export default MoveValidationExample;
@@ -0,0 +1,136 @@
/**
* @file pawn-implementation.js
* @description Complete example implementation of the Pawn class
* Use this as a reference for implementing other pieces
*/
import Piece from '../models/Piece.js';
import { DIRECTIONS } from '../utils/Constants.js';
import { isValidPosition } from '../utils/Helpers.js';
/**
* @class Pawn
* @extends Piece
* @description Implements pawn movement rules
*
* Rules:
* - Moves forward one square (or two from starting position)
* - Captures diagonally forward
* - En passant capture
* - Promotion on reaching opposite end
*/
class Pawn extends Piece {
constructor(color, position) {
super(color, position, 'pawn');
/**
* @property {number} _direction - Movement direction (-1 for white, 1 for black)
*/
this._direction = color === 'white' ? -1 : 1;
/**
* @property {number} _startRow - Starting row (6 for white, 1 for black)
*/
this._startRow = color === 'white' ? 6 : 1;
}
/**
* Gets all valid moves for this pawn
*
* @param {Board} board - Current board state
* @returns {Array<Object>} Array of valid positions
*/
getValidMoves(board) {
const moves = [];
const { row, col } = this.position;
// One square forward
const oneForward = { row: row + this._direction, col };
if (isValidPosition(oneForward.row, oneForward.col)) {
const piece = board.getPieceAt(oneForward);
if (!piece) {
moves.push(oneForward);
// Two squares forward (only from starting position)
if (!this.hasMoved && row === this._startRow) {
const twoForward = { row: row + (2 * this._direction), col };
const pieceTwoForward = board.getPieceAt(twoForward);
if (!pieceTwoForward) {
moves.push(twoForward);
}
}
}
}
// Diagonal captures
const captureOffsets = [-1, 1];
for (const offset of captureOffsets) {
const capturePos = { row: row + this._direction, col: col + offset };
if (isValidPosition(capturePos.row, capturePos.col)) {
const piece = board.getPieceAt(capturePos);
if (piece && piece.color !== this.color) {
moves.push(capturePos);
}
}
}
// TODO: Add en passant logic
// Check if adjacent square has enemy pawn that just moved two squares
// Add the en passant capture move if valid
return moves;
}
/**
* Checks if this pawn can be promoted
*
* @returns {boolean} True if on promotion row
*/
canPromote() {
const promotionRow = this.color === 'white' ? 0 : 7;
return this.position.row === promotionRow;
}
/**
* Clones this pawn
*
* @returns {Pawn} Cloned pawn
*/
clone() {
const clone = new Pawn(this.color, { ...this.position });
clone.hasMoved = this.hasMoved;
return clone;
}
}
export default Pawn;
/**
* IMPLEMENTATION NOTES:
*
* 1. Direction handling:
* - White pawns move "up" the board (row decreases)
* - Black pawns move "down" the board (row increases)
* - Use _direction multiplier to handle both cases
*
* 2. Two-square move:
* - Only allowed from starting position
* - Must check that both squares are empty
* - Sets up potential en passant capture
*
* 3. En passant:
* - Complex special move requiring game state
* - Need to check if adjacent pawn just moved two squares
* - Capture happens "in passing" on empty square
*
* 4. Promotion:
* - Handled by game controller, not move validation
* - Pawn reaches opposite end (row 0 for white, row 7 for black)
* - Player chooses replacement piece (usually queen)
*
* 5. Common bugs to avoid:
* - Forgetting pawns can't move backward
* - Allowing diagonal moves when no capture
* - Allowing capture forward
* - Forgetting to check if two-square path is clear
*/
@@ -0,0 +1,243 @@
/**
* @file sliding-piece-pattern.js
* @description Pattern for implementing sliding pieces (Rook, Bishop, Queen)
* These pieces slide along lines until blocked
*/
import Piece from '../models/Piece.js';
import { DIRECTIONS } from '../utils/Constants.js';
import { isValidPosition } from '../utils/Helpers.js';
/**
* @class Rook
* @extends Piece
* @description Example of sliding piece implementation
*
* Pattern applies to:
* - Rook: DIRECTIONS.ORTHOGONAL (vertical and horizontal)
* - Bishop: DIRECTIONS.DIAGONAL
* - Queen: [...DIRECTIONS.ORTHOGONAL, ...DIRECTIONS.DIAGONAL]
*/
class Rook extends Piece {
constructor(color, position) {
super(color, position, 'rook');
}
/**
* Gets all valid moves for this rook
* Uses sliding piece pattern
*
* @param {Board} board - Current board state
* @returns {Array<Object>} Array of valid positions
*/
getValidMoves(board) {
const moves = [];
// Rook moves in 4 orthogonal directions
const directions = DIRECTIONS.ORTHOGONAL;
// For each direction, slide until blocked
for (const direction of directions) {
const directionMoves = this._getMovesInDirection(board, direction);
moves.push(...directionMoves);
}
return moves;
}
/**
* Gets all moves in a specific direction
*
* @private
* @param {Board} board - Current board state
* @param {Object} direction - Direction vector {row, col}
* @returns {Array<Object>} Valid positions in this direction
*/
_getMovesInDirection(board, direction) {
const moves = [];
const { row, col } = this.position;
let currentRow = row + direction.row;
let currentCol = col + direction.col;
// Slide in direction until we hit edge or piece
while (isValidPosition(currentRow, currentCol)) {
const currentPos = { row: currentRow, col: currentCol };
const piece = board.getPieceAt(currentPos);
if (!piece) {
// Empty square - can move here and continue
moves.push(currentPos);
} else if (piece.color !== this.color) {
// Enemy piece - can capture but can't continue
moves.push(currentPos);
break;
} else {
// Friendly piece - can't move here, stop
break;
}
// Continue sliding
currentRow += direction.row;
currentCol += direction.col;
}
return moves;
}
clone() {
const clone = new Rook(this.color, { ...this.position });
clone.hasMoved = this.hasMoved;
return clone;
}
}
/**
* @class Bishop
* @extends Piece
* @description Bishop using same sliding pattern with diagonal directions
*/
class Bishop extends Piece {
constructor(color, position) {
super(color, position, 'bishop');
}
getValidMoves(board) {
const moves = [];
const directions = DIRECTIONS.DIAGONAL; // Only difference from Rook!
for (const direction of directions) {
const directionMoves = this._getMovesInDirection(board, direction);
moves.push(...directionMoves);
}
return moves;
}
_getMovesInDirection(board, direction) {
// Identical to Rook implementation
const moves = [];
const { row, col } = this.position;
let currentRow = row + direction.row;
let currentCol = col + direction.col;
while (isValidPosition(currentRow, currentCol)) {
const currentPos = { row: currentRow, col: currentCol };
const piece = board.getPieceAt(currentPos);
if (!piece) {
moves.push(currentPos);
} else if (piece.color !== this.color) {
moves.push(currentPos);
break;
} else {
break;
}
currentRow += direction.row;
currentCol += direction.col;
}
return moves;
}
clone() {
const clone = new Bishop(this.color, { ...this.position });
clone.hasMoved = this.hasMoved;
return clone;
}
}
/**
* @class Queen
* @extends Piece
* @description Queen combines Rook + Bishop movements
*/
class Queen extends Piece {
constructor(color, position) {
super(color, position, 'queen');
}
getValidMoves(board) {
const moves = [];
// Queen moves in all 8 directions
const directions = [...DIRECTIONS.ORTHOGONAL, ...DIRECTIONS.DIAGONAL];
for (const direction of directions) {
const directionMoves = this._getMovesInDirection(board, direction);
moves.push(...directionMoves);
}
return moves;
}
_getMovesInDirection(board, direction) {
// Identical to Rook/Bishop implementation
const moves = [];
const { row, col } = this.position;
let currentRow = row + direction.row;
let currentCol = col + direction.col;
while (isValidPosition(currentRow, currentCol)) {
const currentPos = { row: currentRow, col: currentCol };
const piece = board.getPieceAt(currentPos);
if (!piece) {
moves.push(currentPos);
} else if (piece.color !== this.color) {
moves.push(currentPos);
break;
} else {
break;
}
currentRow += direction.row;
currentCol += direction.col;
}
return moves;
}
clone() {
const clone = new Queen(this.color, { ...this.position });
clone.hasMoved = this.hasMoved;
return clone;
}
}
/**
* PATTERN SUMMARY:
*
* All sliding pieces use the same algorithm:
* 1. Define direction vectors
* 2. For each direction:
* a. Start at piece position
* b. Step in direction
* c. Check if position is valid
* d. If empty: add move, continue
* e. If enemy: add move, stop
* f. If friendly: stop
*
* OPTIMIZATION TIP:
* Extract _getMovesInDirection to a shared utility function
* to avoid code duplication:
*
* // In a SlidingPieceHelper.js file:
* export function getSlidingMoves(piece, board, directions) {
* const moves = [];
* for (const direction of directions) {
* moves.push(...getMovesInDirection(piece, board, direction));
* }
* return moves;
* }
*
* // Then in pieces:
* getValidMoves(board) {
* return getSlidingMoves(this, board, DIRECTIONS.ORTHOGONAL);
* }
*/
export { Rook, Bishop, Queen };