schess 1.0.0
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/bin/schess.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +584 -0
- package/package.json +39 -0
- package/src/commands/init.ts +5 -0
- package/src/commands/join.ts +5 -0
- package/src/game/ChessGame.ts +81 -0
- package/src/game/constants.ts +39 -0
- package/src/game/types.ts +26 -0
- package/src/index.ts +27 -0
- package/src/network/Client.ts +112 -0
- package/src/network/NetworkUtils.ts +24 -0
- package/src/network/Protocol.ts +41 -0
- package/src/network/Server.ts +104 -0
- package/src/ui/Board.ts +112 -0
- package/src/ui/GameScreen.ts +183 -0
- package/src/ui/Keyboard.ts +57 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Chess, type Square, type Move } from 'chess.js';
|
|
2
|
+
import type { PlayerColor, PieceInfo } from './types.js';
|
|
3
|
+
|
|
4
|
+
export class ChessGame {
|
|
5
|
+
private chess: Chess;
|
|
6
|
+
|
|
7
|
+
constructor(fen?: string) {
|
|
8
|
+
this.chess = new Chess(fen);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
getPiece(square: Square): PieceInfo | null {
|
|
12
|
+
return this.chess.get(square);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
getValidMoves(square: Square): Square[] {
|
|
16
|
+
const moves = this.chess.moves({ square, verbose: true });
|
|
17
|
+
return moves.map((m) => m.to);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
makeMove(from: Square, to: Square): Move | null {
|
|
21
|
+
try {
|
|
22
|
+
return this.chess.move({ from, to, promotion: 'q' }); // Auto-promote to queen
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
loadMove(from: Square, to: Square): boolean {
|
|
29
|
+
try {
|
|
30
|
+
this.chess.move({ from, to, promotion: 'q' });
|
|
31
|
+
return true;
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getCurrentTurn(): PlayerColor {
|
|
38
|
+
return this.chess.turn();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
isCheck(): boolean {
|
|
42
|
+
return this.chess.isCheck();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
isCheckmate(): boolean {
|
|
46
|
+
return this.chess.isCheckmate();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
isStalemate(): boolean {
|
|
50
|
+
return this.chess.isStalemate();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
isDraw(): boolean {
|
|
54
|
+
return this.chess.isDraw();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
isGameOver(): boolean {
|
|
58
|
+
return this.chess.isGameOver();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getGameOverReason(): string | null {
|
|
62
|
+
if (this.isCheckmate()) return 'Checkmate';
|
|
63
|
+
if (this.isStalemate()) return 'Stalemate';
|
|
64
|
+
if (this.chess.isThreefoldRepetition()) return 'Draw by repetition';
|
|
65
|
+
if (this.chess.isInsufficientMaterial()) return 'Draw by insufficient material';
|
|
66
|
+
if (this.chess.isDraw()) return 'Draw';
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
getFen(): string {
|
|
71
|
+
return this.chess.fen();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
loadFen(fen: string): void {
|
|
75
|
+
this.chess.load(fen);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
reset(): void {
|
|
79
|
+
this.chess.reset();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Unicode chess pieces
|
|
2
|
+
export const PIECES = {
|
|
3
|
+
w: {
|
|
4
|
+
k: '♔',
|
|
5
|
+
q: '♕',
|
|
6
|
+
r: '♖',
|
|
7
|
+
b: '♗',
|
|
8
|
+
n: '♘',
|
|
9
|
+
p: '♙',
|
|
10
|
+
},
|
|
11
|
+
b: {
|
|
12
|
+
k: '♚',
|
|
13
|
+
q: '♛',
|
|
14
|
+
r: '♜',
|
|
15
|
+
b: '♝',
|
|
16
|
+
n: '♞',
|
|
17
|
+
p: '♟',
|
|
18
|
+
},
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
export const FILES = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] as const;
|
|
22
|
+
export const RANKS = ['8', '7', '6', '5', '4', '3', '2', '1'] as const;
|
|
23
|
+
|
|
24
|
+
// Board drawing characters
|
|
25
|
+
export const BOARD_CHARS = {
|
|
26
|
+
topLeft: '╔',
|
|
27
|
+
topRight: '╗',
|
|
28
|
+
bottomLeft: '╚',
|
|
29
|
+
bottomRight: '╝',
|
|
30
|
+
horizontal: '═',
|
|
31
|
+
vertical: '║',
|
|
32
|
+
topT: '╤',
|
|
33
|
+
bottomT: '╧',
|
|
34
|
+
leftT: '╟',
|
|
35
|
+
rightT: '╢',
|
|
36
|
+
cross: '┼',
|
|
37
|
+
innerHorizontal: '───',
|
|
38
|
+
innerVertical: '│',
|
|
39
|
+
} as const;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Chess, Square, Move, Color, PieceSymbol } from 'chess.js';
|
|
2
|
+
|
|
3
|
+
export type PlayerColor = 'w' | 'b';
|
|
4
|
+
|
|
5
|
+
export interface Position {
|
|
6
|
+
row: number; // 0-7 (0 = rank 8, 7 = rank 1)
|
|
7
|
+
col: number; // 0-7 (0 = file a, 7 = file h)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface GameState {
|
|
11
|
+
cursor: Position;
|
|
12
|
+
selectedSquare: Square | null;
|
|
13
|
+
validMoves: Square[];
|
|
14
|
+
lastMove: { from: Square; to: Square } | null;
|
|
15
|
+
isMyTurn: boolean;
|
|
16
|
+
playerColor: PlayerColor;
|
|
17
|
+
gameOver: boolean;
|
|
18
|
+
gameOverReason: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PieceInfo {
|
|
22
|
+
type: PieceSymbol;
|
|
23
|
+
color: Color;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type { Chess, Square, Move, Color, PieceSymbol };
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { initCommand } from './commands/init.js';
|
|
4
|
+
import { joinCommand } from './commands/join.js';
|
|
5
|
+
|
|
6
|
+
const program = new Command();
|
|
7
|
+
|
|
8
|
+
program
|
|
9
|
+
.name('schess')
|
|
10
|
+
.description('CLI Chess game over LAN')
|
|
11
|
+
.version('1.0.0');
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.command('init')
|
|
15
|
+
.description('Host a new chess game')
|
|
16
|
+
.action(async () => {
|
|
17
|
+
await initCommand();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
program
|
|
21
|
+
.command('join <address>')
|
|
22
|
+
.description('Join a chess game (e.g., schess join 192.168.1.100:12345)')
|
|
23
|
+
.action(async (address: string) => {
|
|
24
|
+
await joinCommand(address);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
program.parse();
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { serializeMessage, parseMessage, type GameMessage } from './Protocol.js';
|
|
5
|
+
import { GameScreen } from '../ui/GameScreen.js';
|
|
6
|
+
import type { Square, PlayerColor } from '../game/types.js';
|
|
7
|
+
|
|
8
|
+
export async function connectToServer(address: string): Promise<void> {
|
|
9
|
+
// Parse address
|
|
10
|
+
const [host, portStr] = address.split(':');
|
|
11
|
+
if (!host || !portStr) {
|
|
12
|
+
console.error(chalk.red('Invalid address format. Use: schess join <ip:port>'));
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const port = parseInt(portStr, 10);
|
|
17
|
+
if (isNaN(port)) {
|
|
18
|
+
console.error(chalk.red('Invalid port number'));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log(chalk.bold.cyan('\n ♔ SCHESS - Chess over LAN ♚\n'));
|
|
23
|
+
|
|
24
|
+
const spinner = ora(`Connecting to ${host}:${port}...`).start();
|
|
25
|
+
|
|
26
|
+
const ws = new WebSocket(`ws://${host}:${port}`);
|
|
27
|
+
let gameScreen: GameScreen | null = null;
|
|
28
|
+
|
|
29
|
+
ws.on('open', () => {
|
|
30
|
+
spinner.succeed('Connected to game!');
|
|
31
|
+
ws.send(serializeMessage({ type: 'join' }));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
ws.on('message', (data) => {
|
|
35
|
+
const msg = parseMessage(data.toString());
|
|
36
|
+
if (!msg) return;
|
|
37
|
+
|
|
38
|
+
handleMessage(msg);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
ws.on('close', () => {
|
|
42
|
+
if (gameScreen) {
|
|
43
|
+
gameScreen.opponentResigned();
|
|
44
|
+
setTimeout(() => {
|
|
45
|
+
cleanup();
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}, 3000);
|
|
48
|
+
} else {
|
|
49
|
+
spinner.fail('Disconnected from server');
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
ws.on('error', (err) => {
|
|
55
|
+
spinner.fail(`Connection failed: ${err.message}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
function handleMessage(msg: GameMessage): void {
|
|
60
|
+
switch (msg.type) {
|
|
61
|
+
case 'start': {
|
|
62
|
+
const color: PlayerColor = msg.color;
|
|
63
|
+
console.log(chalk.green(` You are playing as ${color === 'w' ? 'White' : 'Black'}.\n`));
|
|
64
|
+
|
|
65
|
+
gameScreen = new GameScreen(color, {
|
|
66
|
+
onMove: (from: Square, to: Square) => {
|
|
67
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
68
|
+
ws.send(serializeMessage({ type: 'move', from, to }));
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
onResign: () => {
|
|
72
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
73
|
+
ws.send(serializeMessage({ type: 'resign' }));
|
|
74
|
+
}
|
|
75
|
+
cleanup();
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
setTimeout(() => {
|
|
80
|
+
gameScreen?.start();
|
|
81
|
+
}, 500);
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case 'move':
|
|
85
|
+
gameScreen?.receiveMove(msg.from, msg.to);
|
|
86
|
+
break;
|
|
87
|
+
case 'resign':
|
|
88
|
+
gameScreen?.opponentResigned();
|
|
89
|
+
setTimeout(() => {
|
|
90
|
+
cleanup();
|
|
91
|
+
process.exit(0);
|
|
92
|
+
}, 3000);
|
|
93
|
+
break;
|
|
94
|
+
case 'error':
|
|
95
|
+
console.error(chalk.red(`Server error: ${msg.message}`));
|
|
96
|
+
cleanup();
|
|
97
|
+
process.exit(1);
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function cleanup(): void {
|
|
103
|
+
gameScreen?.stop();
|
|
104
|
+
ws.close();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Handle process termination
|
|
108
|
+
process.on('SIGINT', () => {
|
|
109
|
+
cleanup();
|
|
110
|
+
process.exit(0);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { networkInterfaces } from 'os';
|
|
2
|
+
|
|
3
|
+
export function getLanIp(): string | null {
|
|
4
|
+
const interfaces = networkInterfaces();
|
|
5
|
+
|
|
6
|
+
for (const name of Object.keys(interfaces)) {
|
|
7
|
+
const nets = interfaces[name];
|
|
8
|
+
if (!nets) continue;
|
|
9
|
+
|
|
10
|
+
for (const net of nets) {
|
|
11
|
+
// Skip internal (loopback) and non-IPv4 addresses
|
|
12
|
+
if (net.family === 'IPv4' && !net.internal) {
|
|
13
|
+
return net.address;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getRandomPort(): number {
|
|
22
|
+
// Random port between 10000-60000
|
|
23
|
+
return Math.floor(Math.random() * 50000) + 10000;
|
|
24
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Square } from '../game/types.js';
|
|
2
|
+
|
|
3
|
+
export type MessageType = 'join' | 'start' | 'move' | 'resign' | 'error';
|
|
4
|
+
|
|
5
|
+
export interface JoinMessage {
|
|
6
|
+
type: 'join';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface StartMessage {
|
|
10
|
+
type: 'start';
|
|
11
|
+
color: 'w' | 'b';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface MoveMessage {
|
|
15
|
+
type: 'move';
|
|
16
|
+
from: Square;
|
|
17
|
+
to: Square;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ResignMessage {
|
|
21
|
+
type: 'resign';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ErrorMessage {
|
|
25
|
+
type: 'error';
|
|
26
|
+
message: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type GameMessage = JoinMessage | StartMessage | MoveMessage | ResignMessage | ErrorMessage;
|
|
30
|
+
|
|
31
|
+
export function serializeMessage(msg: GameMessage): string {
|
|
32
|
+
return JSON.stringify(msg);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function parseMessage(data: string): GameMessage | null {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(data) as GameMessage;
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { getLanIp, getRandomPort } from './NetworkUtils.js';
|
|
5
|
+
import { serializeMessage, parseMessage, type GameMessage } from './Protocol.js';
|
|
6
|
+
import { GameScreen } from '../ui/GameScreen.js';
|
|
7
|
+
import type { Square } from '../game/types.js';
|
|
8
|
+
|
|
9
|
+
export async function startServer(): Promise<void> {
|
|
10
|
+
const port = getRandomPort();
|
|
11
|
+
const ip = getLanIp() || 'localhost';
|
|
12
|
+
|
|
13
|
+
const wss = new WebSocketServer({ port });
|
|
14
|
+
let opponent: WebSocket | null = null;
|
|
15
|
+
let gameScreen: GameScreen | null = null;
|
|
16
|
+
|
|
17
|
+
console.log(chalk.bold.cyan('\n ♔ SCHESS - Chess over LAN ♚\n'));
|
|
18
|
+
console.log(chalk.green(' Game created! You are playing as White.\n'));
|
|
19
|
+
console.log(chalk.white(' Share this command with your opponent:\n'));
|
|
20
|
+
console.log(chalk.yellow.bold(` schess join ${ip}:${port}\n`));
|
|
21
|
+
|
|
22
|
+
const spinner = ora('Waiting for opponent to join...').start();
|
|
23
|
+
|
|
24
|
+
wss.on('connection', (ws) => {
|
|
25
|
+
if (opponent) {
|
|
26
|
+
ws.send(serializeMessage({ type: 'error', message: 'Game is full' }));
|
|
27
|
+
ws.close();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
opponent = ws;
|
|
32
|
+
spinner.succeed('Opponent connected!');
|
|
33
|
+
|
|
34
|
+
// Send start message - opponent plays as Black
|
|
35
|
+
ws.send(serializeMessage({ type: 'start', color: 'b' }));
|
|
36
|
+
|
|
37
|
+
// Create game screen
|
|
38
|
+
gameScreen = new GameScreen('w', {
|
|
39
|
+
onMove: (from: Square, to: Square) => {
|
|
40
|
+
if (opponent && opponent.readyState === WebSocket.OPEN) {
|
|
41
|
+
opponent.send(serializeMessage({ type: 'move', from, to }));
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
onResign: () => {
|
|
45
|
+
if (opponent && opponent.readyState === WebSocket.OPEN) {
|
|
46
|
+
opponent.send(serializeMessage({ type: 'resign' }));
|
|
47
|
+
}
|
|
48
|
+
cleanup();
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Start the game
|
|
53
|
+
setTimeout(() => {
|
|
54
|
+
gameScreen?.start();
|
|
55
|
+
}, 500);
|
|
56
|
+
|
|
57
|
+
ws.on('message', (data) => {
|
|
58
|
+
const msg = parseMessage(data.toString());
|
|
59
|
+
if (!msg) return;
|
|
60
|
+
|
|
61
|
+
handleMessage(msg);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
ws.on('close', () => {
|
|
65
|
+
if (gameScreen) {
|
|
66
|
+
gameScreen.opponentResigned();
|
|
67
|
+
setTimeout(() => {
|
|
68
|
+
cleanup();
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}, 3000);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
ws.on('error', (err) => {
|
|
75
|
+
console.error(chalk.red('Connection error:'), err.message);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
function handleMessage(msg: GameMessage): void {
|
|
80
|
+
switch (msg.type) {
|
|
81
|
+
case 'move':
|
|
82
|
+
gameScreen?.receiveMove(msg.from, msg.to);
|
|
83
|
+
break;
|
|
84
|
+
case 'resign':
|
|
85
|
+
gameScreen?.opponentResigned();
|
|
86
|
+
setTimeout(() => {
|
|
87
|
+
cleanup();
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}, 3000);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function cleanup(): void {
|
|
95
|
+
gameScreen?.stop();
|
|
96
|
+
wss.close();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Handle process termination
|
|
100
|
+
process.on('SIGINT', () => {
|
|
101
|
+
cleanup();
|
|
102
|
+
process.exit(0);
|
|
103
|
+
});
|
|
104
|
+
}
|
package/src/ui/Board.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { PIECES, FILES, RANKS } from '../game/constants.js';
|
|
3
|
+
import type { ChessGame } from '../game/ChessGame.js';
|
|
4
|
+
import type { GameState, Square, Position, PlayerColor } from '../game/types.js';
|
|
5
|
+
|
|
6
|
+
function positionToSquare(pos: Position): Square {
|
|
7
|
+
return `${FILES[pos.col]}${RANKS[pos.row]}` as Square;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function squareToPosition(square: Square): Position {
|
|
11
|
+
const col = FILES.indexOf(square[0] as typeof FILES[number]);
|
|
12
|
+
const row = RANKS.indexOf(square[1] as typeof RANKS[number]);
|
|
13
|
+
return { row, col };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function renderBoard(game: ChessGame, state: GameState): string {
|
|
17
|
+
const lines: string[] = [];
|
|
18
|
+
const { cursor, selectedSquare, validMoves, lastMove, playerColor } = state;
|
|
19
|
+
|
|
20
|
+
// File labels (a-h)
|
|
21
|
+
const fileLabels = playerColor === 'w'
|
|
22
|
+
? ' a b c d e f g h'
|
|
23
|
+
: ' h g f e d c b a';
|
|
24
|
+
lines.push(chalk.gray(fileLabels));
|
|
25
|
+
|
|
26
|
+
// Top border
|
|
27
|
+
lines.push(chalk.gray(' ╔═══╤═══╤═══╤═══╤═══╤═══╤═══╤═══╗'));
|
|
28
|
+
|
|
29
|
+
// Determine row/col iteration based on player color
|
|
30
|
+
const rowOrder = playerColor === 'w' ? [0, 1, 2, 3, 4, 5, 6, 7] : [7, 6, 5, 4, 3, 2, 1, 0];
|
|
31
|
+
const colOrder = playerColor === 'w' ? [0, 1, 2, 3, 4, 5, 6, 7] : [7, 6, 5, 4, 3, 2, 1, 0];
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < 8; i++) {
|
|
34
|
+
const row = rowOrder[i];
|
|
35
|
+
const rank = RANKS[row];
|
|
36
|
+
let line = chalk.gray(` ${rank} ║`);
|
|
37
|
+
|
|
38
|
+
for (let j = 0; j < 8; j++) {
|
|
39
|
+
const col = colOrder[j];
|
|
40
|
+
const square = positionToSquare({ row, col });
|
|
41
|
+
const piece = game.getPiece(square);
|
|
42
|
+
|
|
43
|
+
// Determine cell content
|
|
44
|
+
let cellContent = ' ';
|
|
45
|
+
if (piece) {
|
|
46
|
+
const pieceChar = PIECES[piece.color][piece.type];
|
|
47
|
+
cellContent = ` ${pieceChar} `;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Apply highlighting
|
|
51
|
+
const isCursor = cursor.row === row && cursor.col === col;
|
|
52
|
+
const isSelected = selectedSquare === square;
|
|
53
|
+
const isValidMove = validMoves.includes(square);
|
|
54
|
+
const isLastMoveFrom = lastMove?.from === square;
|
|
55
|
+
const isLastMoveTo = lastMove?.to === square;
|
|
56
|
+
|
|
57
|
+
if (isCursor) {
|
|
58
|
+
cellContent = chalk.bgYellow.black(cellContent);
|
|
59
|
+
} else if (isSelected) {
|
|
60
|
+
cellContent = chalk.bgRgb(255, 140, 0).black(cellContent); // Orange
|
|
61
|
+
} else if (isValidMove) {
|
|
62
|
+
cellContent = chalk.bgGreen.black(cellContent);
|
|
63
|
+
} else if (isLastMoveFrom || isLastMoveTo) {
|
|
64
|
+
cellContent = chalk.bgBlue.white(cellContent);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
line += cellContent;
|
|
68
|
+
line += j < 7 ? chalk.gray('│') : chalk.gray('║');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
lines.push(line);
|
|
72
|
+
|
|
73
|
+
// Row separator or bottom border
|
|
74
|
+
if (i < 7) {
|
|
75
|
+
lines.push(chalk.gray(' ╟───┼───┼───┼───┼───┼───┼───┼───╢'));
|
|
76
|
+
} else {
|
|
77
|
+
lines.push(chalk.gray(' ╚═══╧═══╧═══╧═══╧═══╧═══╧═══╧═══╝'));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return lines.join('\n');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function renderStatus(game: ChessGame, state: GameState): string {
|
|
85
|
+
const turn = game.getCurrentTurn();
|
|
86
|
+
const turnText = turn === 'w' ? 'White' : 'Black';
|
|
87
|
+
const yourTurn = state.isMyTurn ? chalk.green('Your turn') : chalk.yellow('Opponent\'s turn');
|
|
88
|
+
|
|
89
|
+
let status = ` ${turnText} to move | ${yourTurn}`;
|
|
90
|
+
|
|
91
|
+
if (game.isCheck()) {
|
|
92
|
+
status += chalk.red(' | CHECK!');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (state.gameOver) {
|
|
96
|
+
status = chalk.bold.red(` Game Over: ${state.gameOverReason}`);
|
|
97
|
+
const winner = state.gameOverReason === 'Checkmate'
|
|
98
|
+
? (turn === 'w' ? 'Black wins!' : 'White wins!')
|
|
99
|
+
: '';
|
|
100
|
+
if (winner) {
|
|
101
|
+
status += chalk.bold.yellow(` ${winner}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return status;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function renderControls(): string {
|
|
109
|
+
return chalk.gray(' ↑↓←→: Move | Enter: Select | Esc: Cancel | Q: Quit');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export { positionToSquare, squareToPosition };
|