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:
co-authored by
Claude
parent
1fd28d10b4
commit
5ad0700b41
@@ -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 };
|
||||
Reference in New Issue
Block a user