Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions react-ystemandchess/src/core/types/chess.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export type PieceSymbol = 'p' | 'n' | 'b' | 'r' | 'q' | 'k';
export type PlayerColor = "white" | "black";
export type GameMode = "regular" | "puzzle" | "lesson";
export type UserRole = "mentor" | "student" | "host" | "guest";
Expand Down
117 changes: 117 additions & 0 deletions react-ystemandchess/src/core/types/goals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import type { Chess } from 'chess.js';
import { PieceSymbol } from './chess';

export type Actor = 'player' | 'opponent';

export type AtomicGoal =
| {
type: 'PROMOTION';
min?: number;
piece?: 'q' | 'r' | 'b' | 'n';
by?: Actor; // Defaults to 'player'
}
| {
type: 'CAPTURE';
min?: number;
piece?: 'p' | 'n' | 'b' | 'r' | 'q';
square?: string;
by?: Actor;
}
| {
type: 'CHECKMATE';
by?: Actor;
}
| {
type: 'PAWN_DOUBLE_PUSH';
min?: number;
by?: Actor;
}
| {
type: 'ALL_PAWNS_MOVED';
by?: Actor;
}
| {
type: 'MATERIAL_ADVANTAGE';
threshold: number;
};


export type CompositeGoal =
| {
type: 'AND';
goals: Goal[];
}
| {
type: 'OR';
goals: Goal[];
}


export type SequenceGoal = {
type: 'SEQUENCE';
goals: AtomicGoal[];
};

export type Goal = AtomicGoal | CompositeGoal | SequenceGoal;

export type OpponentConstraint =
| {
type: 'AVOID_SQUARES';
squares: string[];
}
| {
type: 'AVOID_CAPTURING';
pieces?: ('p' | 'n' | 'b' | 'r' | 'q' | 'k')[]; // If omitted, avoid all captures
}
| {
type: 'AVOID_CHECKING';
}
| {
type: 'ONLY_MOVE_PIECES';
pieces: ('p' | 'n' | 'b' | 'r' | 'q' | 'k')[];
}
| {
type: 'STAY_IN_AREA';
minRank?: number; // e.g., 6 = stay on ranks 6-8
maxRank?: number; // e.g., 3 = stay on ranks 1-3
}
| {
type: 'DONT_MOVE_FROM';
squares: string[]; // Don't move pieces away from these squares
};

export interface MoveEvent {
san: string;
from: string;
to: string;
piece: PieceSymbol;
captured?: PieceSymbol;
promotion?: PieceSymbol;
check: boolean;
checkmate: boolean;
doublePawnPush: boolean;
enPassant: boolean;
castling?: 'kingside' | 'queenside';
fen: string;
by?: Actor;
}

export interface EvaluationContext {
events: MoveEvent[];
currentGame: Chess;
startFen: string;
currentFen: string;
playerColor: 'white' | 'black';
moveCount: number;
}

export interface LessonData {
lessonNum: number;
name: string;
startFen: string;
info: string;
solution?: string;
goal?: Goal;
maxMoves?: number;
opponentConstraints?: OpponentConstraint[];
}
92 changes: 92 additions & 0 deletions react-ystemandchess/src/core/utils/eventLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { MoveEvent } from '../types/goals';
import { PieceSymbol } from '../types/chess';

type ChessJsMove = {
san: string;
from: string;
to: string;
piece: PieceSymbol;
captured?: PieceSymbol;
promotion?: PieceSymbol;
flags: string;
};

export function createMoveEvent(
move: ChessJsMove,
afterFen: string,
isPlayerMove: boolean
): MoveEvent {
const fromRank = Number(move.from[1]);
const toRank = Number(move.to[1]);

const doublePawnPush =
move.piece === 'p' &&
Math.abs(fromRank - toRank) === 2;

return {
san: move.san,
from: move.from,
to: move.to,
piece: move.piece,
captured: move.captured,
promotion: move.promotion,
check: move.san.includes('+'),
checkmate: move.san.includes('#'),
doublePawnPush,
enPassant: move.flags.includes('e'),
castling: move.flags.includes('k')
? 'kingside'
: move.flags.includes('q')
? 'queenside'
: undefined,
fen: afterFen,
by: isPlayerMove ? 'player' : 'opponent'
};
}

export class EventLog {
private events: MoveEvent[] = [];

addMove(event: MoveEvent) {
this.events.push(event);
}

getEvents(): MoveEvent[] {
return [...this.events];
}

clear() {
this.events = [];
}

getPromotions(): MoveEvent[] {
return this.events.filter(e => e.promotion);
}

getCaptures(): MoveEvent[] {
return this.events.filter(e => e.captured);
}

getDoublePawnPushes(): MoveEvent[] {
return this.events.filter(e => e.doublePawnPush);
}

hasCheckmate(): boolean {
return this.events.some(e => e.checkmate);
}

hasSequence(
checkA: (e: MoveEvent) => boolean,
checkB: (e: MoveEvent) => boolean
): boolean {
let foundA = false;
for (const event of this.events) {
if (!foundA && checkA(event)) {
foundA = true;
} else if (foundA && checkB(event)) {
return true;
}
}
return false;
}
}
Loading