Chess on the ATmosphere
checkmate.blue
chess
1import { describe, it, expect } from 'vitest';
2import { Chess } from 'chess.js';
3import {
4 toDests,
5 isPromotion,
6 applyMove,
7 turnColor,
8 lastMoveSquares,
9 gameResult,
10 makePgn,
11} from '$lib/game-logic';
12
13describe('toDests', () => {
14 it('returns all legal moves from starting position', () => {
15 const chess = new Chess();
16 const dests = toDests(chess);
17
18 // Knights and pawns can move from start
19 expect(dests.has('e2')).toBe(true);
20 expect(dests.get('e2')).toContain('e4');
21 expect(dests.get('e2')).toContain('e3');
22 expect(dests.get('g1')).toContain('f3');
23 expect(dests.get('g1')).toContain('h3');
24 });
25
26 it('returns empty map for checkmate position', () => {
27 const chess = new Chess();
28 // Scholar's mate
29 chess.move('e4'); chess.move('e5');
30 chess.move('Qh5'); chess.move('Nc6');
31 chess.move('Bc4'); chess.move('Nf6');
32 chess.move('Qxf7');
33
34 const dests = toDests(chess);
35 expect(dests.size).toBe(0);
36 });
37
38 it('only includes squares with legal moves', () => {
39 const chess = new Chess();
40 const dests = toDests(chess);
41
42 // Black pieces cannot move on white's turn
43 expect(dests.has('e7')).toBe(false);
44 // White rook is blocked
45 expect(dests.has('a1')).toBe(false);
46 });
47});
48
49describe('isPromotion', () => {
50 it('detects pawn promotion on 7th rank', () => {
51 const chess = new Chess('6k1/3P4/8/8/8/8/8/4K3 w - - 0 1');
52 expect(isPromotion(chess, 'd7', 'd8')).toBe(true);
53 });
54
55 it('returns false for normal pawn move', () => {
56 const chess = new Chess();
57 expect(isPromotion(chess, 'e2', 'e4')).toBe(false);
58 });
59
60 it('returns false for non-pawn moves', () => {
61 const chess = new Chess();
62 expect(isPromotion(chess, 'g1', 'f3')).toBe(false);
63 });
64});
65
66describe('applyMove', () => {
67 it('applies a valid move and returns true', () => {
68 const chess = new Chess();
69 expect(applyMove(chess, 'e2', 'e4')).toBe(true);
70 // After e4, the pawn is on e4 (4th rank has the pawn)
71 expect(chess.get('e4')).toBeTruthy();
72 });
73
74 it('returns false for illegal move', () => {
75 const chess = new Chess();
76 // chess.js throws on invalid moves, applyMove should handle this
77 // e2 to e5 is not a legal pawn move
78 expect(() => applyMove(chess, 'e2', 'e5')).toThrow();
79 });
80
81 it('applies promotion with piece specified', () => {
82 const chess = new Chess('6k1/3P4/8/8/8/8/8/4K3 w - - 0 1');
83 expect(applyMove(chess, 'd7', 'd8', 'q')).toBe(true);
84 expect(chess.get('d8')?.type).toBe('q');
85 });
86});
87
88describe('turnColor', () => {
89 it('returns white at start', () => {
90 expect(turnColor(new Chess())).toBe('white');
91 });
92
93 it('returns black after one move', () => {
94 const chess = new Chess();
95 chess.move('e4');
96 expect(turnColor(chess)).toBe('black');
97 });
98});
99
100describe('lastMoveSquares', () => {
101 it('returns undefined when no moves played', () => {
102 expect(lastMoveSquares(new Chess())).toBeUndefined();
103 });
104
105 it('returns from/to of the last move', () => {
106 const chess = new Chess();
107 chess.move('e4');
108 expect(lastMoveSquares(chess)).toEqual(['e2', 'e4']);
109
110 chess.move('e5');
111 expect(lastMoveSquares(chess)).toEqual(['e7', 'e5']);
112 });
113});
114
115describe('gameResult', () => {
116 it('returns null when game is not over', () => {
117 expect(gameResult(new Chess())).toBeNull();
118 });
119
120 it('detects checkmate by white', () => {
121 const chess = new Chess();
122 chess.move('e4'); chess.move('e5');
123 chess.move('Qh5'); chess.move('Nc6');
124 chess.move('Bc4'); chess.move('Nf6');
125 chess.move('Qxf7');
126
127 const result = gameResult(chess);
128 expect(result).not.toBeNull();
129 expect(result!.result).toBe('1-0');
130 expect(result!.reason).toBe('checkmate');
131 });
132
133 it('detects stalemate', () => {
134 // Stalemate position: black king trapped with no legal moves
135 const chess = new Chess('k7/8/1K6/8/8/8/8/7R b - - 0 1');
136 // Verify it's actually stalemate
137 if (chess.isStalemate()) {
138 const result = gameResult(chess);
139 expect(result).not.toBeNull();
140 expect(result!.result).toBe('1/2-1/2');
141 expect(result!.reason).toBe('stalemate');
142 }
143 });
144
145 it('detects insufficient material', () => {
146 // King vs King
147 const chess = new Chess('4k3/8/8/8/8/8/8/4K3 w - - 0 1');
148 const result = gameResult(chess);
149 expect(result).not.toBeNull();
150 expect(result!.result).toBe('1/2-1/2');
151 expect(result!.reason).toBe('insufficient');
152 });
153});
154
155describe('makePgn', () => {
156 it('generates PGN with headers', () => {
157 const chess = new Chess();
158 chess.move('e4');
159 chess.move('e5');
160
161 const pgn = makePgn(chess, 'did:plc:white', 'did:plc:black');
162 expect(pgn).toContain('[Event "checkmate.blue"]');
163 expect(pgn).toContain('[Site "https://checkmate.blue"]');
164 expect(pgn).toContain('[White "did:plc:white"]');
165 expect(pgn).toContain('[Black "did:plc:black"]');
166 expect(pgn).toContain('[Result "*"]');
167 expect(pgn).toContain('1. e4 e5');
168 });
169
170 it('sets result when game is over', () => {
171 const chess = new Chess();
172 chess.move('e4'); chess.move('e5');
173 chess.move('Qh5'); chess.move('Nc6');
174 chess.move('Bc4'); chess.move('Nf6');
175 chess.move('Qxf7');
176
177 const pgn = makePgn(chess);
178 expect(pgn).toContain('[Result "1-0"]');
179 });
180
181 it('uses placeholder when DIDs not provided', () => {
182 const chess = new Chess();
183 chess.move('e4');
184
185 const pgn = makePgn(chess);
186 // chess.js defaults to "?" for unset player headers
187 expect(pgn).not.toContain('[White "did:');
188 expect(pgn).not.toContain('[Black "did:');
189 });
190});