| |
| |
| |
| |
|
|
| import { Emitter } from '../core/events'; |
| import type { State } from '../core/state'; |
| import type { Drill, EvaluationResult } from './practice_types'; |
| import { findDrill } from './drill_generator'; |
| import { evaluate } from './evaluation'; |
|
|
| |
| |
| export interface PracticeApi { |
| applyMoves(moves: string[] | string): { accepted: string[]; rejected: string[] }; |
| getState(): State; |
| isSolved(): boolean; |
| onMove(fn: (move: string, state: State) => void): () => void; |
| } |
|
|
| export interface PracticeView { |
| drill: Drill; |
| round: number; |
| roundCount: number; |
| score: number; |
| completed: boolean; |
| evaluation: EvaluationResult; |
| } |
|
|
| export type PracticeState = |
| | { drill: null } |
| | ({ drill: Drill } & PracticeView); |
|
|
| export class PracticeEngine { |
| private readonly api: PracticeApi; |
| private readonly drills: Drill[]; |
| private readonly emitter = new Emitter<PracticeState>(); |
| private readonly unsubscribeMove: () => void; |
|
|
| private current: Drill | null = null; |
| private round = 0; |
| private score = 0; |
| private completed = false; |
| private moveHistory: string[] = []; |
| |
| |
| private pendingSetupMoves = 0; |
|
|
| constructor(api: PracticeApi, drills: Drill[]) { |
| this.api = api; |
| this.drills = drills; |
| this.unsubscribeMove = api.onMove((move) => this.handleMove(move)); |
| } |
|
|
| getDrills(): Drill[] { |
| return this.drills.slice(); |
| } |
|
|
| getCurrentDrill(): Drill | null { |
| return this.current; |
| } |
|
|
| subscribe(fn: (state: PracticeState) => void): () => void { |
| const off = this.emitter.on(fn); |
| fn(this.snapshot()); |
| return off; |
| } |
|
|
| selectDrill(id: string): void { |
| const drill = findDrill(this.drills, id); |
| if (!drill) return; |
| this.current = drill; |
| this.startRound(0, 0); |
| } |
|
|
| closeDrill(): void { |
| this.current = null; |
| this.moveHistory = []; |
| this.pendingSetupMoves = 0; |
| this.emit(); |
| } |
|
|
| resetDrill(): void { |
| if (!this.current) return; |
| this.startRound(0, 0); |
| } |
|
|
| applySetupMoves(): void { |
| if (!this.current?.setupMoves?.length) return; |
| this.pendingSetupMoves += this.current.setupMoves.length; |
| this.api.applyMoves(this.current.setupMoves); |
| } |
|
|
| applyExampleMoves(): void { |
| if (!this.current?.expectedMoves?.length) return; |
| this.api.applyMoves(this.current.expectedMoves); |
| } |
|
|
| |
| |
| handleCubeReset(): void { |
| this.moveHistory = []; |
| this.pendingSetupMoves = 0; |
| this.emit(); |
| } |
|
|
| dispose(): void { |
| this.unsubscribeMove(); |
| } |
|
|
| private roundCount(): number { |
| return this.current?.rounds ?? 1; |
| } |
|
|
| |
| |
| private startRound(round: number, score: number): void { |
| if (!this.current) return; |
| this.round = round; |
| this.score = score; |
| this.completed = false; |
| this.moveHistory = []; |
| this.pendingSetupMoves = 0; |
| if (this.current.setupMoves?.length) { |
| this.pendingSetupMoves += this.current.setupMoves.length; |
| this.api.applyMoves(this.current.setupMoves); |
| } |
| this.emit(); |
| } |
|
|
| private handleMove(move: string): void { |
| if (!this.current || this.completed) return; |
| if (this.pendingSetupMoves > 0) { |
| this.pendingSetupMoves -= 1; |
| |
| |
| if (this.pendingSetupMoves === 0) this.emit(); |
| return; |
| } |
| this.moveHistory.push(move); |
| const result = evaluate(this.current, this.moveHistory, this.api.getState()); |
| if (result.status === 'correct') { |
| this.completeRound(); |
| } else { |
| this.emit(); |
| } |
| } |
|
|
| private completeRound(): void { |
| if (!this.current) return; |
| const nextScore = this.score + 1; |
| if (this.round < this.roundCount() - 1) { |
| |
| this.startRound(this.round + 1, nextScore); |
| } else { |
| this.score = nextScore; |
| this.completed = true; |
| this.moveHistory = []; |
| this.emit(); |
| } |
| } |
|
|
| private snapshot(): PracticeState { |
| if (!this.current) return { drill: null }; |
| const evaluation: EvaluationResult = this.completed |
| ? { status: 'correct', message: 'Drill complete.' } |
| : evaluate(this.current, this.moveHistory, this.api.getState()); |
| return { |
| drill: this.current, |
| round: this.round, |
| roundCount: this.roundCount(), |
| score: this.score, |
| completed: this.completed, |
| evaluation |
| }; |
| } |
|
|
| private emit(): void { |
| this.emitter.emit(this.snapshot()); |
| } |
| } |
|
|