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,183 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { ChessGame } from '../game/ChessGame.js';
|
|
3
|
+
import type { GameState, PlayerColor, Square, Position } from '../game/types.js';
|
|
4
|
+
import { renderBoard, renderStatus, renderControls, positionToSquare, squareToPosition } from './Board.js';
|
|
5
|
+
import { createKeyboardHandler, type KeyAction } from './Keyboard.js';
|
|
6
|
+
|
|
7
|
+
export interface NetworkCallbacks {
|
|
8
|
+
onMove: (from: Square, to: Square) => void;
|
|
9
|
+
onResign: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class GameScreen {
|
|
13
|
+
private game: ChessGame;
|
|
14
|
+
private state: GameState;
|
|
15
|
+
private keyboard: ReturnType<typeof createKeyboardHandler>;
|
|
16
|
+
private networkCallbacks: NetworkCallbacks;
|
|
17
|
+
|
|
18
|
+
constructor(playerColor: PlayerColor, callbacks: NetworkCallbacks) {
|
|
19
|
+
this.game = new ChessGame();
|
|
20
|
+
this.networkCallbacks = callbacks;
|
|
21
|
+
|
|
22
|
+
// Initialize cursor at center
|
|
23
|
+
const startRow = playerColor === 'w' ? 6 : 1; // Start near own pieces
|
|
24
|
+
this.state = {
|
|
25
|
+
cursor: { row: startRow, col: 4 },
|
|
26
|
+
selectedSquare: null,
|
|
27
|
+
validMoves: [],
|
|
28
|
+
lastMove: null,
|
|
29
|
+
isMyTurn: playerColor === 'w', // White goes first
|
|
30
|
+
playerColor,
|
|
31
|
+
gameOver: false,
|
|
32
|
+
gameOverReason: null,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
this.keyboard = createKeyboardHandler(this.handleKey.bind(this));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private handleKey(action: KeyAction): void {
|
|
39
|
+
if (this.state.gameOver) {
|
|
40
|
+
if (action === 'quit' || action === 'enter') {
|
|
41
|
+
this.stop();
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
switch (action) {
|
|
48
|
+
case 'up':
|
|
49
|
+
this.moveCursor(-1, 0);
|
|
50
|
+
break;
|
|
51
|
+
case 'down':
|
|
52
|
+
this.moveCursor(1, 0);
|
|
53
|
+
break;
|
|
54
|
+
case 'left':
|
|
55
|
+
this.moveCursor(0, -1);
|
|
56
|
+
break;
|
|
57
|
+
case 'right':
|
|
58
|
+
this.moveCursor(0, 1);
|
|
59
|
+
break;
|
|
60
|
+
case 'enter':
|
|
61
|
+
this.handleSelect();
|
|
62
|
+
break;
|
|
63
|
+
case 'escape':
|
|
64
|
+
this.cancelSelection();
|
|
65
|
+
break;
|
|
66
|
+
case 'quit':
|
|
67
|
+
this.networkCallbacks.onResign();
|
|
68
|
+
this.stop();
|
|
69
|
+
console.log(chalk.yellow('\nYou resigned. Game over.'));
|
|
70
|
+
process.exit(0);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.render();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private moveCursor(dRow: number, dCol: number): void {
|
|
78
|
+
// Adjust for board orientation
|
|
79
|
+
const actualDRow = this.state.playerColor === 'w' ? dRow : -dRow;
|
|
80
|
+
const actualDCol = this.state.playerColor === 'w' ? dCol : -dCol;
|
|
81
|
+
|
|
82
|
+
const newRow = Math.max(0, Math.min(7, this.state.cursor.row + actualDRow));
|
|
83
|
+
const newCol = Math.max(0, Math.min(7, this.state.cursor.col + actualDCol));
|
|
84
|
+
this.state.cursor = { row: newRow, col: newCol };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private handleSelect(): void {
|
|
88
|
+
if (!this.state.isMyTurn) return;
|
|
89
|
+
|
|
90
|
+
const cursorSquare = positionToSquare(this.state.cursor);
|
|
91
|
+
|
|
92
|
+
if (this.state.selectedSquare) {
|
|
93
|
+
// Try to make a move
|
|
94
|
+
if (this.state.validMoves.includes(cursorSquare)) {
|
|
95
|
+
const move = this.game.makeMove(this.state.selectedSquare, cursorSquare);
|
|
96
|
+
if (move) {
|
|
97
|
+
this.state.lastMove = { from: this.state.selectedSquare, to: cursorSquare };
|
|
98
|
+
this.networkCallbacks.onMove(this.state.selectedSquare, cursorSquare);
|
|
99
|
+
this.state.isMyTurn = false;
|
|
100
|
+
|
|
101
|
+
// Check game over
|
|
102
|
+
if (this.game.isGameOver()) {
|
|
103
|
+
this.state.gameOver = true;
|
|
104
|
+
this.state.gameOverReason = this.game.getGameOverReason();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
this.cancelSelection();
|
|
108
|
+
} else {
|
|
109
|
+
// Clicked on a different square - check if it's own piece
|
|
110
|
+
const piece = this.game.getPiece(cursorSquare);
|
|
111
|
+
if (piece && piece.color === this.state.playerColor) {
|
|
112
|
+
this.selectPiece(cursorSquare);
|
|
113
|
+
} else {
|
|
114
|
+
this.cancelSelection();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
// Select a piece
|
|
119
|
+
const piece = this.game.getPiece(cursorSquare);
|
|
120
|
+
if (piece && piece.color === this.state.playerColor) {
|
|
121
|
+
this.selectPiece(cursorSquare);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private selectPiece(square: Square): void {
|
|
127
|
+
this.state.selectedSquare = square;
|
|
128
|
+
this.state.validMoves = this.game.getValidMoves(square);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private cancelSelection(): void {
|
|
132
|
+
this.state.selectedSquare = null;
|
|
133
|
+
this.state.validMoves = [];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Called when opponent makes a move
|
|
137
|
+
receiveMove(from: Square, to: Square): void {
|
|
138
|
+
this.game.loadMove(from, to);
|
|
139
|
+
this.state.lastMove = { from, to };
|
|
140
|
+
this.state.isMyTurn = true;
|
|
141
|
+
|
|
142
|
+
// Check game over
|
|
143
|
+
if (this.game.isGameOver()) {
|
|
144
|
+
this.state.gameOver = true;
|
|
145
|
+
this.state.gameOverReason = this.game.getGameOverReason();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this.render();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Called when opponent resigns
|
|
152
|
+
opponentResigned(): void {
|
|
153
|
+
this.state.gameOver = true;
|
|
154
|
+
this.state.gameOverReason = 'Opponent resigned';
|
|
155
|
+
this.render();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
render(): void {
|
|
159
|
+
// Clear screen
|
|
160
|
+
console.clear();
|
|
161
|
+
|
|
162
|
+
// Title
|
|
163
|
+
console.log(chalk.bold.cyan('\n ♔ SCHESS - Chess over LAN ♚\n'));
|
|
164
|
+
|
|
165
|
+
// Board
|
|
166
|
+
console.log(renderBoard(this.game, this.state));
|
|
167
|
+
console.log();
|
|
168
|
+
|
|
169
|
+
// Status
|
|
170
|
+
console.log(renderStatus(this.game, this.state));
|
|
171
|
+
console.log(renderControls());
|
|
172
|
+
console.log();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
start(): void {
|
|
176
|
+
this.keyboard.start();
|
|
177
|
+
this.render();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
stop(): void {
|
|
181
|
+
this.keyboard.stop();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as readline from 'readline';
|
|
2
|
+
|
|
3
|
+
export type KeyAction = 'up' | 'down' | 'left' | 'right' | 'enter' | 'escape' | 'quit' | 'unknown';
|
|
4
|
+
|
|
5
|
+
export interface KeyboardHandler {
|
|
6
|
+
onKey: (action: KeyAction) => void;
|
|
7
|
+
start: () => void;
|
|
8
|
+
stop: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createKeyboardHandler(onKey: (action: KeyAction) => void): KeyboardHandler {
|
|
12
|
+
let isRunning = false;
|
|
13
|
+
|
|
14
|
+
const handleKeypress = (_str: string, key: readline.Key): void => {
|
|
15
|
+
if (!isRunning) return;
|
|
16
|
+
|
|
17
|
+
let action: KeyAction = 'unknown';
|
|
18
|
+
|
|
19
|
+
if (key.name === 'up') action = 'up';
|
|
20
|
+
else if (key.name === 'down') action = 'down';
|
|
21
|
+
else if (key.name === 'left') action = 'left';
|
|
22
|
+
else if (key.name === 'right') action = 'right';
|
|
23
|
+
else if (key.name === 'return') action = 'enter';
|
|
24
|
+
else if (key.name === 'escape') action = 'escape';
|
|
25
|
+
else if (key.name === 'q' || (key.ctrl && key.name === 'c')) action = 'quit';
|
|
26
|
+
|
|
27
|
+
if (action !== 'unknown') {
|
|
28
|
+
onKey(action);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
onKey,
|
|
34
|
+
start: () => {
|
|
35
|
+
if (isRunning) return;
|
|
36
|
+
isRunning = true;
|
|
37
|
+
|
|
38
|
+
// Enable raw mode
|
|
39
|
+
if (process.stdin.isTTY) {
|
|
40
|
+
readline.emitKeypressEvents(process.stdin);
|
|
41
|
+
process.stdin.setRawMode(true);
|
|
42
|
+
process.stdin.resume();
|
|
43
|
+
process.stdin.on('keypress', handleKeypress);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
stop: () => {
|
|
47
|
+
if (!isRunning) return;
|
|
48
|
+
isRunning = false;
|
|
49
|
+
|
|
50
|
+
if (process.stdin.isTTY) {
|
|
51
|
+
process.stdin.setRawMode(false);
|
|
52
|
+
process.stdin.removeListener('keypress', handleKeypress);
|
|
53
|
+
process.stdin.pause();
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"resolveJsonModule": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist"]
|
|
20
|
+
}
|