ddd-team1 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/package.json +35 -0
- package/src/application/AuthService.ts +3 -0
- package/src/application/GameRepository.ts +11 -0
- package/src/application/GetGameStateQuery.ts +46 -0
- package/src/application/MakeMoveUseCase.ts +22 -0
- package/src/application/OnlineGameService.ts +17 -0
- package/src/domain/Command.ts +12 -0
- package/src/domain/DomainEvent.ts +20 -0
- package/src/domain/Game.ts +72 -0
- package/src/domain/shared/AggregateRoot.ts +21 -0
- package/src/domain/shared/BusinessRules/BusinessRule.ts +16 -0
- package/src/domain/shared/BusinessRules/BusinessRuleViolation.ts +7 -0
- package/src/domain/shared/BusinessRules/BusinessRuleViolationError.ts +10 -0
- package/src/domain/shared/Entity.ts +7 -0
- package/src/domain/shared/ValueObject.ts +20 -0
- package/src/domain/shared/index.ts +6 -0
- package/src/domain/value-objects/Board.ts +197 -0
- package/src/domain/value-objects/Piece.ts +91 -0
- package/src/domain/value-objects/PieceV2.ts +79 -0
- package/src/domain/value-objects/Position.ts +80 -0
- package/src/domain/value-objects/StandardAlgebraicNotationMove.ts +168 -0
- package/src/domain/value-objects/pieces/Rook.ts +31 -0
- package/src/infrastructure/FileBasedGameRepository.ts +50 -0
- package/src/infrastructure/InMemoryGameRepository.ts +29 -0
- package/src/infrastructure/firebase/FirebaseAuthService.ts +15 -0
- package/src/infrastructure/firebase/FirestoreGameRepository.ts +67 -0
- package/src/infrastructure/firebase/FirestoreOnlineGameService.ts +117 -0
- package/src/infrastructure/firebase/configStore.ts +85 -0
- package/src/infrastructure/firebase/filePersistence.ts +30 -0
- package/src/infrastructure/firebase/firebaseConfig.ts +41 -0
- package/src/infrastructure/tui/App.tsx +159 -0
- package/src/infrastructure/tui/GameRepositoryContext.tsx +48 -0
- package/src/infrastructure/tui/components/BoardView.tsx +46 -0
- package/src/infrastructure/tui/components/GameMenu.tsx +171 -0
- package/src/infrastructure/tui/components/GameScreen.tsx +184 -0
- package/src/infrastructure/tui/components/HostGameScreen.tsx +83 -0
- package/src/infrastructure/tui/components/JoinGameScreen.tsx +84 -0
- package/src/infrastructure/tui/components/LogMessages.tsx +51 -0
- package/src/infrastructure/tui/components/MoveInput.tsx +140 -0
- package/src/infrastructure/tui/components/SetupScreen.tsx +85 -0
- package/src/infrastructure/tui/index.tsx +70 -0
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# DDD Chess
|
|
2
|
+
|
|
3
|
+
A Domain-Driven Design chess implementation in TypeScript.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- [Bun](https://bun.sh/) (v1.0+)
|
|
8
|
+
|
|
9
|
+
## Getting Started
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun install
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Playing
|
|
16
|
+
|
|
17
|
+
Start the terminal UI:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bun start
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Enter moves in algebraic notation, e.g. `e2 e4` to move a piece from e2 to e4.
|
|
24
|
+
|
|
25
|
+
## Running Tests
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
bun test
|
|
29
|
+
bun test --watch
|
|
30
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ddd-team1",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"ddd-team1": "./src/infrastructure/tui/index.tsx"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"src/**/*.ts",
|
|
10
|
+
"src/**/*.tsx",
|
|
11
|
+
"!src/**/*.test.ts"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"bun": ">=1.0.0"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"start": "bun src/infrastructure/tui/index.tsx",
|
|
21
|
+
"test": "bun test",
|
|
22
|
+
"test:watch": "bun test --watch"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/bun": "latest",
|
|
26
|
+
"@types/react": "^19.2.13",
|
|
27
|
+
"typescript": "^5.7.0"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"firebase": "^12.9.0",
|
|
31
|
+
"ink": "^6.7.0",
|
|
32
|
+
"react": "^19.2.4",
|
|
33
|
+
"reflect-metadata": "^0.2.2"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { UUID } from 'crypto';
|
|
2
|
+
import type { DomainEvent } from '../domain/DomainEvent.ts';
|
|
3
|
+
import type { Game } from '../domain/Game.ts';
|
|
4
|
+
|
|
5
|
+
export interface GameRepository {
|
|
6
|
+
save(events: DomainEvent[]): Promise<void>;
|
|
7
|
+
save(event: DomainEvent): Promise<void>;
|
|
8
|
+
findById(id: UUID): Promise<Game | null>;
|
|
9
|
+
getEvents(id: UUID): Promise<DomainEvent[]>;
|
|
10
|
+
list(): Promise<Array<UUID>>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { UUID } from 'crypto';
|
|
2
|
+
import { Game } from '../domain/Game.ts';
|
|
3
|
+
import type { GameRepository } from './GameRepository.ts';
|
|
4
|
+
|
|
5
|
+
export interface PieceDTO {
|
|
6
|
+
color: 'white' | 'black';
|
|
7
|
+
icon: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface EventDTO {
|
|
11
|
+
name: string;
|
|
12
|
+
payload?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface GameStateDTO {
|
|
16
|
+
id: UUID;
|
|
17
|
+
turn: number;
|
|
18
|
+
currentPlayer: 'white' | 'black';
|
|
19
|
+
board: (PieceDTO | null)[][];
|
|
20
|
+
events: EventDTO[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class GetGameStateQuery {
|
|
24
|
+
constructor(private repo: GameRepository) {}
|
|
25
|
+
|
|
26
|
+
async execute(gameId: UUID): Promise<GameStateDTO> {
|
|
27
|
+
const game = await this.repo.findById(gameId) ?? new Game(gameId);
|
|
28
|
+
const domainEvents = await this.repo.getEvents(gameId);
|
|
29
|
+
return {
|
|
30
|
+
id: gameId,
|
|
31
|
+
turn: game.currentTurn(),
|
|
32
|
+
currentPlayer: game.currentColor(),
|
|
33
|
+
board: game.getBoardState().getState().map((row) =>
|
|
34
|
+
row.map((piece) => (piece ? {color: piece.color, icon: piece.icon} : null)),
|
|
35
|
+
),
|
|
36
|
+
events: domainEvents.map((e) => ({
|
|
37
|
+
name: e.name,
|
|
38
|
+
...('payload' in e ? { payload: e.payload } : {}),
|
|
39
|
+
})),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async list(): Promise<GameStateDTO[]> {
|
|
44
|
+
return Promise.all((await this.repo.list()).map((id) => this.execute(id)));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Game } from '../domain/Game.ts';
|
|
2
|
+
import {
|
|
3
|
+
StandardAlgebraicNotationMove,
|
|
4
|
+
standardAlgebraicNotationString,
|
|
5
|
+
type StandardAlgebraicNotationString
|
|
6
|
+
} from '../domain/value-objects/StandardAlgebraicNotationMove.ts';
|
|
7
|
+
import type { GameRepository } from './GameRepository.ts';
|
|
8
|
+
export class MakeMoveUseCase {
|
|
9
|
+
constructor(private repo: GameRepository) {}
|
|
10
|
+
|
|
11
|
+
async execute(gameId: Game['id'], standardAlgebraicNotation: string): Promise<void> {
|
|
12
|
+
let game = await this.repo.findById(gameId) ?? new Game(gameId);
|
|
13
|
+
const command = {
|
|
14
|
+
name: 'Make move!' as const,
|
|
15
|
+
payload: {
|
|
16
|
+
move: StandardAlgebraicNotationMove.fromString(standardAlgebraicNotationString(`${game?.currentTurn()}. ${standardAlgebraicNotation.trim()}`)),
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
game.execute(command);
|
|
20
|
+
await this.repo.save(game.flushEvents());
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface GameInfo {
|
|
2
|
+
gameId: string;
|
|
3
|
+
hostUid: string;
|
|
4
|
+
hostColor: 'white' | 'black';
|
|
5
|
+
guestUid: string | null;
|
|
6
|
+
status: 'waiting' | 'active' | 'finished';
|
|
7
|
+
createdAt: Date;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface OnlineGameService {
|
|
11
|
+
createGame(hostUid: string): Promise<GameInfo>;
|
|
12
|
+
joinGame(gameId: string, guestUid: string): Promise<GameInfo>;
|
|
13
|
+
listHostedGames(uid: string): Promise<GameInfo[]>;
|
|
14
|
+
listJoinedGames(uid: string): Promise<GameInfo[]>;
|
|
15
|
+
onGameInfoChanged(gameId: string, callback: (info: GameInfo) => void): () => void;
|
|
16
|
+
onEventsChanged(gameId: string, callback: (events: { index: number; name: string; aggregateId: string; payload?: string }[]) => void): () => void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { StandardAlgebraicNotationMove } from './value-objects/StandardAlgebraicNotationMove.ts';
|
|
2
|
+
|
|
3
|
+
export type Command = MakeMoveCommand
|
|
4
|
+
|
|
5
|
+
export type CommandBase<CommandName extends `${string}!`, Payload = never> = {
|
|
6
|
+
name: CommandName
|
|
7
|
+
payload: Payload
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type MakeMoveCommand = CommandBase<'Make move!', {
|
|
11
|
+
move: StandardAlgebraicNotationMove
|
|
12
|
+
}>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { UUID } from 'crypto'
|
|
2
|
+
import type {
|
|
3
|
+
StandardAlgebraicNotationMove,
|
|
4
|
+
StandardAlgebraicNotationString
|
|
5
|
+
} from './value-objects/StandardAlgebraicNotationMove.ts';
|
|
6
|
+
|
|
7
|
+
export type EventBase<EventName extends `${string}.`, Payload = never, AggregateId = UUID> = [Payload] extends [never] ? {
|
|
8
|
+
name: EventName
|
|
9
|
+
aggregateId: AggregateId
|
|
10
|
+
} : {
|
|
11
|
+
name: EventName
|
|
12
|
+
aggregateId: AggregateId
|
|
13
|
+
payload: Payload
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type GameStartedEvent = EventBase<'Game started.'>;
|
|
17
|
+
|
|
18
|
+
export type MoveMadeEvent = EventBase<'Move made.', StandardAlgebraicNotationString>;
|
|
19
|
+
|
|
20
|
+
export type DomainEvent = GameStartedEvent | MoveMadeEvent;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { UUID } from 'crypto';
|
|
2
|
+
import type { Command } from './Command.ts';
|
|
3
|
+
import type { DomainEvent, GameStartedEvent, MoveMadeEvent } from './DomainEvent.ts';
|
|
4
|
+
import { AggregateRoot } from './shared';
|
|
5
|
+
import { Board } from './value-objects/Board.ts';
|
|
6
|
+
import { StandardAlgebraicNotationMove } from './value-objects/StandardAlgebraicNotationMove.ts';
|
|
7
|
+
|
|
8
|
+
export class Game extends AggregateRoot<UUID> {
|
|
9
|
+
private board = new Board();
|
|
10
|
+
|
|
11
|
+
constructor(id: UUID) {
|
|
12
|
+
super(id);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
static fromEvents(id: UUID, events: DomainEvent[]): Game {
|
|
16
|
+
const game = new Game(id);
|
|
17
|
+
game.replay(events);
|
|
18
|
+
return game;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
replay(events: DomainEvent[]): void {
|
|
22
|
+
events
|
|
23
|
+
.filter(event => event.aggregateId === this.id)
|
|
24
|
+
.forEach(event => {
|
|
25
|
+
if (event.name === 'Move made.') {
|
|
26
|
+
this.board = this.board.play(StandardAlgebraicNotationMove.fromString(event.payload));
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
move(move: StandardAlgebraicNotationMove): DomainEvent[] {
|
|
32
|
+
if (this.board.equals(new Board())) {
|
|
33
|
+
this.recordThat({
|
|
34
|
+
name: 'Game started.',
|
|
35
|
+
aggregateId: this.id,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.recordThat({
|
|
40
|
+
name: 'Move made.',
|
|
41
|
+
aggregateId: this.id,
|
|
42
|
+
payload: move.toString(),
|
|
43
|
+
});
|
|
44
|
+
this.replay(this.newlyRecordEvents);
|
|
45
|
+
return this.newlyRecordEvents;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
execute(command: Command): DomainEvent[] {
|
|
49
|
+
switch (command.name) {
|
|
50
|
+
case 'Make move!':
|
|
51
|
+
return this.move(command.payload.move);
|
|
52
|
+
default:
|
|
53
|
+
throw new Error(`Unknown command: ${command.name}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getBoardState(): Board {
|
|
58
|
+
return this.board;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getBoardStateFormatted(): string {
|
|
62
|
+
return this.board.getBoardStateFormatted();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
currentColor() {
|
|
66
|
+
return this.board.color;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
currentTurn() {
|
|
70
|
+
return this.board.turn;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Entity } from "./Entity";
|
|
2
|
+
import type { DomainEvent } from "../DomainEvent.ts";
|
|
3
|
+
|
|
4
|
+
export abstract class AggregateRoot<T> extends Entity<T>
|
|
5
|
+
{
|
|
6
|
+
protected newlyRecordEvents: DomainEvent[] = [];
|
|
7
|
+
|
|
8
|
+
flushEvents(): DomainEvent[] {
|
|
9
|
+
const eventsToFlush = [...this.newlyRecordEvents];
|
|
10
|
+
this.newlyRecordEvents = [];
|
|
11
|
+
return eventsToFlush;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
constructor(id: T){
|
|
15
|
+
super(id);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
recordThat(event: DomainEvent): void {
|
|
19
|
+
this.newlyRecordEvents.push(event);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BusinessRuleViolation, BusinessRuleViolationError } from "..";
|
|
2
|
+
|
|
3
|
+
export abstract class BusinessRule {
|
|
4
|
+
public abstract checkRule(): BusinessRuleViolation[]
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class BusinessRules {
|
|
8
|
+
public static ThrowIfAnyNotSatisfied(...businessRules: BusinessRule[]) : void {
|
|
9
|
+
|
|
10
|
+
let violations = businessRules.flatMap(rule => rule.checkRule());
|
|
11
|
+
|
|
12
|
+
if(violations && violations.length > 0) {
|
|
13
|
+
throw new BusinessRuleViolationError(violations);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BusinessRuleViolation } from "..";
|
|
2
|
+
|
|
3
|
+
export class BusinessRuleViolationError extends Error {
|
|
4
|
+
public violations: BusinessRuleViolation[];
|
|
5
|
+
|
|
6
|
+
public constructor(violations: BusinessRuleViolation[]) {
|
|
7
|
+
super();
|
|
8
|
+
this.violations = violations;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export abstract class ValueObject {
|
|
2
|
+
protected abstract getAtomicValues() : any[];
|
|
3
|
+
|
|
4
|
+
public equals(obj: any) {
|
|
5
|
+
if (!(obj instanceof ValueObject)) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
let thisValues = this.getAtomicValues();
|
|
10
|
+
let thatValues = obj.getAtomicValues();
|
|
11
|
+
|
|
12
|
+
if(thisValues.length !== thatValues.length) return false;
|
|
13
|
+
|
|
14
|
+
for(let i = 0; i < thisValues.length; i++){
|
|
15
|
+
if(thisValues[i] !== thatValues[i]) return false;
|
|
16
|
+
}
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type { StandardAlgebraicNotationMove } from "./StandardAlgebraicNotationMove.ts";
|
|
2
|
+
import { Piece } from "./Piece.ts";
|
|
3
|
+
import { Position } from "./Position.ts";
|
|
4
|
+
import { ValueObject } from "../shared";
|
|
5
|
+
import { Rook } from "./pieces/Rook.ts";
|
|
6
|
+
import type { PieceV2 } from "./PieceV2.ts";
|
|
7
|
+
|
|
8
|
+
type BoardState = (Piece | PieceV2 | null)[][];
|
|
9
|
+
|
|
10
|
+
export class Board extends ValueObject {
|
|
11
|
+
static initialBoardState: BoardState = [
|
|
12
|
+
[
|
|
13
|
+
new Rook("white"),
|
|
14
|
+
Piece.WhiteKnight,
|
|
15
|
+
Piece.WhiteBishop,
|
|
16
|
+
Piece.WhiteQueen,
|
|
17
|
+
Piece.WhiteKing,
|
|
18
|
+
Piece.WhiteBishop,
|
|
19
|
+
Piece.WhiteKnight,
|
|
20
|
+
new Rook("white"),
|
|
21
|
+
],
|
|
22
|
+
[
|
|
23
|
+
Piece.WhitePawn,
|
|
24
|
+
Piece.WhitePawn,
|
|
25
|
+
Piece.WhitePawn,
|
|
26
|
+
Piece.WhitePawn,
|
|
27
|
+
Piece.WhitePawn,
|
|
28
|
+
Piece.WhitePawn,
|
|
29
|
+
Piece.WhitePawn,
|
|
30
|
+
Piece.WhitePawn,
|
|
31
|
+
],
|
|
32
|
+
[null, null, null, null, null, null, null, null],
|
|
33
|
+
[null, null, null, null, null, null, null, null],
|
|
34
|
+
[null, null, null, null, null, null, null, null],
|
|
35
|
+
[null, null, null, null, null, null, null, null],
|
|
36
|
+
[
|
|
37
|
+
Piece.BlackPawn,
|
|
38
|
+
Piece.BlackPawn,
|
|
39
|
+
Piece.BlackPawn,
|
|
40
|
+
Piece.BlackPawn,
|
|
41
|
+
Piece.BlackPawn,
|
|
42
|
+
Piece.BlackPawn,
|
|
43
|
+
Piece.BlackPawn,
|
|
44
|
+
Piece.BlackPawn,
|
|
45
|
+
],
|
|
46
|
+
[
|
|
47
|
+
new Rook("black"),
|
|
48
|
+
Piece.BlackKnight,
|
|
49
|
+
Piece.BlackBishop,
|
|
50
|
+
Piece.BlackQueen,
|
|
51
|
+
Piece.BlackKing,
|
|
52
|
+
Piece.BlackBishop,
|
|
53
|
+
Piece.BlackKnight,
|
|
54
|
+
new Rook("black"),
|
|
55
|
+
],
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
constructor(
|
|
59
|
+
protected boardState: BoardState = Board.initialBoardState,
|
|
60
|
+
public readonly turn = 1,
|
|
61
|
+
) {
|
|
62
|
+
super();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get color(): "white" | "black" {
|
|
66
|
+
return this.turn % 2 === 1 ? "white" : "black";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
play(move: StandardAlgebraicNotationMove): Board {
|
|
70
|
+
if (move.isCastling) {
|
|
71
|
+
throw new Error("Castling not yet implemented");
|
|
72
|
+
}
|
|
73
|
+
const newBoardState = this.boardState.map((row) => row.map((cell) => cell));
|
|
74
|
+
const to = move.destination!;
|
|
75
|
+
const from = this.getFromPosition(move);
|
|
76
|
+
newBoardState[to.rank]![to.file] =
|
|
77
|
+
this.boardState[from.rank]![from.file] ?? null;
|
|
78
|
+
newBoardState[from.rank]![from.file] = null;
|
|
79
|
+
return new Board(newBoardState, this.turn + 1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getState(): BoardState {
|
|
83
|
+
return this.boardState;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getBoardStateFormatted(): string {
|
|
87
|
+
const rows = this.boardState
|
|
88
|
+
.toReversed()
|
|
89
|
+
.map(
|
|
90
|
+
(row, i) =>
|
|
91
|
+
`${8 - i} ` + row.map((cell) => cell?.icon ?? "·").join(" "),
|
|
92
|
+
);
|
|
93
|
+
return "\n" + [...rows, " a b c d e f g h"].join("\n");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
//TODO: alleen raamwerk overlaten, validatie gaat naar het stuk (piece). betere naam bedenken
|
|
97
|
+
/**
|
|
98
|
+
* Determine FROM position based on the TO position
|
|
99
|
+
* array met posities > als meer dan 1 dan fout.
|
|
100
|
+
*/
|
|
101
|
+
getFromPosition(move: StandardAlgebraicNotationMove): Position {
|
|
102
|
+
let rulesToCheck: Position[][] = [];
|
|
103
|
+
const position = move.destination;
|
|
104
|
+
|
|
105
|
+
if (!position) {
|
|
106
|
+
throw new Error("Move must have a destination");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const capturedPiece = this.boardState[position.rank]![position.file];
|
|
110
|
+
if (capturedPiece?.color == this.color) {
|
|
111
|
+
throw new Error("You cannot capture yourself");
|
|
112
|
+
}
|
|
113
|
+
if (move.piece instanceof Piece) {
|
|
114
|
+
if (move.piece?.type === "pawn" && this.color === "white") {
|
|
115
|
+
if (capturedPiece !== null) {
|
|
116
|
+
if (position.file > 0) rulesToCheck.push([position.offset(-1, -1)]);
|
|
117
|
+
if (position.file < 7) rulesToCheck.push([position.offset(1, -1)]);
|
|
118
|
+
} else {
|
|
119
|
+
let forwardMoves = [position.offset(0, -1)];
|
|
120
|
+
if (position.rank === 3) {
|
|
121
|
+
forwardMoves.push(position.offset(0, -2));
|
|
122
|
+
}
|
|
123
|
+
rulesToCheck.push(forwardMoves);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (move.piece?.type === "pawn" && this.color === "black") {
|
|
128
|
+
if (capturedPiece !== null) {
|
|
129
|
+
if (position.file > 0) rulesToCheck.push([position.offset(-1, 1)]);
|
|
130
|
+
if (position.file < 7) rulesToCheck.push([position.offset(1, 1)]);
|
|
131
|
+
} else {
|
|
132
|
+
let forwardMoves = [position.offset(0, 1)];
|
|
133
|
+
if (position.rank === 4) {
|
|
134
|
+
forwardMoves.push(position.offset(0, 2));
|
|
135
|
+
}
|
|
136
|
+
rulesToCheck.push(forwardMoves);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (move.piece?.type === "bishop") {
|
|
141
|
+
rulesToCheck = position.diagonalMask;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (move.piece?.type === "knight") {
|
|
145
|
+
rulesToCheck = position.jumpMask;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (move.piece?.type === "queen") {
|
|
149
|
+
rulesToCheck = position.straightMask.concat(position.diagonalMask);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (move.piece?.type === "king") {
|
|
153
|
+
rulesToCheck = position.straightMask
|
|
154
|
+
.slice(0, 1)
|
|
155
|
+
.concat(position.diagonalMask.slice(0, 1));
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
if (move.piece instanceof Rook) {
|
|
159
|
+
rulesToCheck = move.piece.getRulesToCheck(position);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/*
|
|
164
|
+
* checks each position in the rules to see if the piece at that position matches the move's piece, and returns
|
|
165
|
+
* the first match aborts the line if it encounters a piece that doesn't match, since that would block the move
|
|
166
|
+
*
|
|
167
|
+
* TODO: this is a very naive implementation and doesn't account for many rules of chess (e.g. check, pins, en
|
|
168
|
+
* passant, etc.) but it should work for basic moves. Also, this assumes that the move's piece is correctly set
|
|
169
|
+
* to the piece that is actually moving, which may not always be the case (e.g. in a promotion), so this might need
|
|
170
|
+
* to be adjusted in the future
|
|
171
|
+
*/
|
|
172
|
+
for (const lineToCheck of rulesToCheck) {
|
|
173
|
+
for (const position of lineToCheck) {
|
|
174
|
+
if (
|
|
175
|
+
position.file < 0 ||
|
|
176
|
+
position.file >= 8 ||
|
|
177
|
+
position.rank < 0 ||
|
|
178
|
+
position.rank >= 8
|
|
179
|
+
) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const pieceAtPosition =
|
|
183
|
+
this.boardState[position.rank]![position.file] ?? null;
|
|
184
|
+
if (move.piece?.equals(pieceAtPosition)) return position;
|
|
185
|
+
if (pieceAtPosition !== null) break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
throw new Error(
|
|
190
|
+
"No valid from position found for move: " + move.toString(),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
protected getAtomicValues(): any[] {
|
|
195
|
+
return [this.boardState, this.turn];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { ValueObject } from '../shared';
|
|
2
|
+
|
|
3
|
+
export type PieceColor = 'white' | 'black';
|
|
4
|
+
export type PieceType = 'king' | 'queen' | 'rook' | 'bishop' | 'knight' | 'pawn';
|
|
5
|
+
|
|
6
|
+
const ICONS: Record<PieceColor, Record<PieceType, string>> = {
|
|
7
|
+
black: { king: '♔', queen: '♕', rook: '♖', bishop: '♗', knight: '♘', pawn: '♙' },
|
|
8
|
+
white: { king: '♚', queen: '♛', rook: '♜', bishop: '♝', knight: '♞', pawn: '♟' },
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const PIECE_LETTER: Record<string, PieceType> = {
|
|
12
|
+
K: 'king',
|
|
13
|
+
Q: 'queen',
|
|
14
|
+
R: 'rook',
|
|
15
|
+
B: 'bishop',
|
|
16
|
+
N: 'knight',
|
|
17
|
+
'': 'pawn',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const LETTER_PIECE: Record<PieceType, string> = {
|
|
21
|
+
king: 'K',
|
|
22
|
+
queen: 'Q',
|
|
23
|
+
rook: 'R',
|
|
24
|
+
bishop: 'B',
|
|
25
|
+
knight: 'N',
|
|
26
|
+
pawn: '',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class Piece extends ValueObject {
|
|
30
|
+
private constructor(
|
|
31
|
+
public readonly color: PieceColor,
|
|
32
|
+
public readonly type: PieceType,
|
|
33
|
+
) {
|
|
34
|
+
super();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get icon(): string {
|
|
38
|
+
return ICONS[this.color][this.type];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
override toString(): string {
|
|
42
|
+
return `${this.icon} (${this.color} ${this.type})`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
toIcon(): string {
|
|
46
|
+
return this.icon;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
toChar(): string {
|
|
50
|
+
return LETTER_PIECE[this.type];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
protected getAtomicValues(): any[] {
|
|
54
|
+
return [this.color, this.type];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static fromIcon(icon: string): Piece | null {
|
|
58
|
+
for (const color in ICONS) {
|
|
59
|
+
for (const type in ICONS[color as PieceColor]) {
|
|
60
|
+
if (ICONS[color as PieceColor][type as PieceType] === icon) {
|
|
61
|
+
return new Piece(color as PieceColor, type as PieceType);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
static fromLetter(letter: string, color: 'white' | 'black'): Piece | null {
|
|
69
|
+
const pieceType = PIECE_LETTER[letter.toUpperCase()];
|
|
70
|
+
if (!pieceType) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return new Piece(color, pieceType);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// White pieces
|
|
77
|
+
static readonly WhiteKing = new Piece('white', 'king');
|
|
78
|
+
static readonly WhiteQueen = new Piece('white', 'queen');
|
|
79
|
+
static readonly WhiteRook = new Piece('white', 'rook');
|
|
80
|
+
static readonly WhiteBishop = new Piece('white', 'bishop');
|
|
81
|
+
static readonly WhiteKnight = new Piece('white', 'knight');
|
|
82
|
+
static readonly WhitePawn = new Piece('white', 'pawn');
|
|
83
|
+
|
|
84
|
+
// Black pieces
|
|
85
|
+
static readonly BlackKing = new Piece('black', 'king');
|
|
86
|
+
static readonly BlackQueen = new Piece('black', 'queen');
|
|
87
|
+
static readonly BlackRook = new Piece('black', 'rook');
|
|
88
|
+
static readonly BlackBishop = new Piece('black', 'bishop');
|
|
89
|
+
static readonly BlackKnight = new Piece('black', 'knight');
|
|
90
|
+
static readonly BlackPawn = new Piece('black', 'pawn');
|
|
91
|
+
}
|