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 ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/index.js';
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,584 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/network/Server.ts
7
+ import { WebSocketServer, WebSocket } from "ws";
8
+ import chalk3 from "chalk";
9
+ import ora from "ora";
10
+
11
+ // src/network/NetworkUtils.ts
12
+ import { networkInterfaces } from "os";
13
+ function getLanIp() {
14
+ const interfaces = networkInterfaces();
15
+ for (const name of Object.keys(interfaces)) {
16
+ const nets = interfaces[name];
17
+ if (!nets) continue;
18
+ for (const net of nets) {
19
+ if (net.family === "IPv4" && !net.internal) {
20
+ return net.address;
21
+ }
22
+ }
23
+ }
24
+ return null;
25
+ }
26
+ function getRandomPort() {
27
+ return Math.floor(Math.random() * 5e4) + 1e4;
28
+ }
29
+
30
+ // src/network/Protocol.ts
31
+ function serializeMessage(msg) {
32
+ return JSON.stringify(msg);
33
+ }
34
+ function parseMessage(data) {
35
+ try {
36
+ return JSON.parse(data);
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ // src/ui/GameScreen.ts
43
+ import chalk2 from "chalk";
44
+
45
+ // src/game/ChessGame.ts
46
+ import { Chess } from "chess.js";
47
+ var ChessGame = class {
48
+ chess;
49
+ constructor(fen) {
50
+ this.chess = new Chess(fen);
51
+ }
52
+ getPiece(square) {
53
+ return this.chess.get(square);
54
+ }
55
+ getValidMoves(square) {
56
+ const moves = this.chess.moves({ square, verbose: true });
57
+ return moves.map((m) => m.to);
58
+ }
59
+ makeMove(from, to) {
60
+ try {
61
+ return this.chess.move({ from, to, promotion: "q" });
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+ loadMove(from, to) {
67
+ try {
68
+ this.chess.move({ from, to, promotion: "q" });
69
+ return true;
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+ getCurrentTurn() {
75
+ return this.chess.turn();
76
+ }
77
+ isCheck() {
78
+ return this.chess.isCheck();
79
+ }
80
+ isCheckmate() {
81
+ return this.chess.isCheckmate();
82
+ }
83
+ isStalemate() {
84
+ return this.chess.isStalemate();
85
+ }
86
+ isDraw() {
87
+ return this.chess.isDraw();
88
+ }
89
+ isGameOver() {
90
+ return this.chess.isGameOver();
91
+ }
92
+ getGameOverReason() {
93
+ if (this.isCheckmate()) return "Checkmate";
94
+ if (this.isStalemate()) return "Stalemate";
95
+ if (this.chess.isThreefoldRepetition()) return "Draw by repetition";
96
+ if (this.chess.isInsufficientMaterial()) return "Draw by insufficient material";
97
+ if (this.chess.isDraw()) return "Draw";
98
+ return null;
99
+ }
100
+ getFen() {
101
+ return this.chess.fen();
102
+ }
103
+ loadFen(fen) {
104
+ this.chess.load(fen);
105
+ }
106
+ reset() {
107
+ this.chess.reset();
108
+ }
109
+ };
110
+
111
+ // src/ui/Board.ts
112
+ import chalk from "chalk";
113
+
114
+ // src/game/constants.ts
115
+ var PIECES = {
116
+ w: {
117
+ k: "\u2654",
118
+ q: "\u2655",
119
+ r: "\u2656",
120
+ b: "\u2657",
121
+ n: "\u2658",
122
+ p: "\u2659"
123
+ },
124
+ b: {
125
+ k: "\u265A",
126
+ q: "\u265B",
127
+ r: "\u265C",
128
+ b: "\u265D",
129
+ n: "\u265E",
130
+ p: "\u265F"
131
+ }
132
+ };
133
+ var FILES = ["a", "b", "c", "d", "e", "f", "g", "h"];
134
+ var RANKS = ["8", "7", "6", "5", "4", "3", "2", "1"];
135
+
136
+ // src/ui/Board.ts
137
+ function positionToSquare(pos) {
138
+ return `${FILES[pos.col]}${RANKS[pos.row]}`;
139
+ }
140
+ function renderBoard(game, state) {
141
+ const lines = [];
142
+ const { cursor, selectedSquare, validMoves, lastMove, playerColor } = state;
143
+ const fileLabels = playerColor === "w" ? " a b c d e f g h" : " h g f e d c b a";
144
+ lines.push(chalk.gray(fileLabels));
145
+ lines.push(chalk.gray(" \u2554\u2550\u2550\u2550\u2564\u2550\u2550\u2550\u2564\u2550\u2550\u2550\u2564\u2550\u2550\u2550\u2564\u2550\u2550\u2550\u2564\u2550\u2550\u2550\u2564\u2550\u2550\u2550\u2564\u2550\u2550\u2550\u2557"));
146
+ const rowOrder = playerColor === "w" ? [0, 1, 2, 3, 4, 5, 6, 7] : [7, 6, 5, 4, 3, 2, 1, 0];
147
+ const colOrder = playerColor === "w" ? [0, 1, 2, 3, 4, 5, 6, 7] : [7, 6, 5, 4, 3, 2, 1, 0];
148
+ for (let i = 0; i < 8; i++) {
149
+ const row = rowOrder[i];
150
+ const rank = RANKS[row];
151
+ let line = chalk.gray(` ${rank} \u2551`);
152
+ for (let j = 0; j < 8; j++) {
153
+ const col = colOrder[j];
154
+ const square = positionToSquare({ row, col });
155
+ const piece = game.getPiece(square);
156
+ let cellContent = " ";
157
+ if (piece) {
158
+ const pieceChar = PIECES[piece.color][piece.type];
159
+ cellContent = ` ${pieceChar} `;
160
+ }
161
+ const isCursor = cursor.row === row && cursor.col === col;
162
+ const isSelected = selectedSquare === square;
163
+ const isValidMove = validMoves.includes(square);
164
+ const isLastMoveFrom = lastMove?.from === square;
165
+ const isLastMoveTo = lastMove?.to === square;
166
+ if (isCursor) {
167
+ cellContent = chalk.bgYellow.black(cellContent);
168
+ } else if (isSelected) {
169
+ cellContent = chalk.bgRgb(255, 140, 0).black(cellContent);
170
+ } else if (isValidMove) {
171
+ cellContent = chalk.bgGreen.black(cellContent);
172
+ } else if (isLastMoveFrom || isLastMoveTo) {
173
+ cellContent = chalk.bgBlue.white(cellContent);
174
+ }
175
+ line += cellContent;
176
+ line += j < 7 ? chalk.gray("\u2502") : chalk.gray("\u2551");
177
+ }
178
+ lines.push(line);
179
+ if (i < 7) {
180
+ lines.push(chalk.gray(" \u255F\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2562"));
181
+ } else {
182
+ lines.push(chalk.gray(" \u255A\u2550\u2550\u2550\u2567\u2550\u2550\u2550\u2567\u2550\u2550\u2550\u2567\u2550\u2550\u2550\u2567\u2550\u2550\u2550\u2567\u2550\u2550\u2550\u2567\u2550\u2550\u2550\u2567\u2550\u2550\u2550\u255D"));
183
+ }
184
+ }
185
+ return lines.join("\n");
186
+ }
187
+ function renderStatus(game, state) {
188
+ const turn = game.getCurrentTurn();
189
+ const turnText = turn === "w" ? "White" : "Black";
190
+ const yourTurn = state.isMyTurn ? chalk.green("Your turn") : chalk.yellow("Opponent's turn");
191
+ let status = ` ${turnText} to move | ${yourTurn}`;
192
+ if (game.isCheck()) {
193
+ status += chalk.red(" | CHECK!");
194
+ }
195
+ if (state.gameOver) {
196
+ status = chalk.bold.red(` Game Over: ${state.gameOverReason}`);
197
+ const winner = state.gameOverReason === "Checkmate" ? turn === "w" ? "Black wins!" : "White wins!" : "";
198
+ if (winner) {
199
+ status += chalk.bold.yellow(` ${winner}`);
200
+ }
201
+ }
202
+ return status;
203
+ }
204
+ function renderControls() {
205
+ return chalk.gray(" \u2191\u2193\u2190\u2192: Move | Enter: Select | Esc: Cancel | Q: Quit");
206
+ }
207
+
208
+ // src/ui/Keyboard.ts
209
+ import * as readline from "readline";
210
+ function createKeyboardHandler(onKey) {
211
+ let isRunning = false;
212
+ const handleKeypress = (_str, key) => {
213
+ if (!isRunning) return;
214
+ let action = "unknown";
215
+ if (key.name === "up") action = "up";
216
+ else if (key.name === "down") action = "down";
217
+ else if (key.name === "left") action = "left";
218
+ else if (key.name === "right") action = "right";
219
+ else if (key.name === "return") action = "enter";
220
+ else if (key.name === "escape") action = "escape";
221
+ else if (key.name === "q" || key.ctrl && key.name === "c") action = "quit";
222
+ if (action !== "unknown") {
223
+ onKey(action);
224
+ }
225
+ };
226
+ return {
227
+ onKey,
228
+ start: () => {
229
+ if (isRunning) return;
230
+ isRunning = true;
231
+ if (process.stdin.isTTY) {
232
+ readline.emitKeypressEvents(process.stdin);
233
+ process.stdin.setRawMode(true);
234
+ process.stdin.resume();
235
+ process.stdin.on("keypress", handleKeypress);
236
+ }
237
+ },
238
+ stop: () => {
239
+ if (!isRunning) return;
240
+ isRunning = false;
241
+ if (process.stdin.isTTY) {
242
+ process.stdin.setRawMode(false);
243
+ process.stdin.removeListener("keypress", handleKeypress);
244
+ process.stdin.pause();
245
+ }
246
+ }
247
+ };
248
+ }
249
+
250
+ // src/ui/GameScreen.ts
251
+ var GameScreen = class {
252
+ game;
253
+ state;
254
+ keyboard;
255
+ networkCallbacks;
256
+ constructor(playerColor, callbacks) {
257
+ this.game = new ChessGame();
258
+ this.networkCallbacks = callbacks;
259
+ const startRow = playerColor === "w" ? 6 : 1;
260
+ this.state = {
261
+ cursor: { row: startRow, col: 4 },
262
+ selectedSquare: null,
263
+ validMoves: [],
264
+ lastMove: null,
265
+ isMyTurn: playerColor === "w",
266
+ // White goes first
267
+ playerColor,
268
+ gameOver: false,
269
+ gameOverReason: null
270
+ };
271
+ this.keyboard = createKeyboardHandler(this.handleKey.bind(this));
272
+ }
273
+ handleKey(action) {
274
+ if (this.state.gameOver) {
275
+ if (action === "quit" || action === "enter") {
276
+ this.stop();
277
+ process.exit(0);
278
+ }
279
+ return;
280
+ }
281
+ switch (action) {
282
+ case "up":
283
+ this.moveCursor(-1, 0);
284
+ break;
285
+ case "down":
286
+ this.moveCursor(1, 0);
287
+ break;
288
+ case "left":
289
+ this.moveCursor(0, -1);
290
+ break;
291
+ case "right":
292
+ this.moveCursor(0, 1);
293
+ break;
294
+ case "enter":
295
+ this.handleSelect();
296
+ break;
297
+ case "escape":
298
+ this.cancelSelection();
299
+ break;
300
+ case "quit":
301
+ this.networkCallbacks.onResign();
302
+ this.stop();
303
+ console.log(chalk2.yellow("\nYou resigned. Game over."));
304
+ process.exit(0);
305
+ break;
306
+ }
307
+ this.render();
308
+ }
309
+ moveCursor(dRow, dCol) {
310
+ const actualDRow = this.state.playerColor === "w" ? dRow : -dRow;
311
+ const actualDCol = this.state.playerColor === "w" ? dCol : -dCol;
312
+ const newRow = Math.max(0, Math.min(7, this.state.cursor.row + actualDRow));
313
+ const newCol = Math.max(0, Math.min(7, this.state.cursor.col + actualDCol));
314
+ this.state.cursor = { row: newRow, col: newCol };
315
+ }
316
+ handleSelect() {
317
+ if (!this.state.isMyTurn) return;
318
+ const cursorSquare = positionToSquare(this.state.cursor);
319
+ if (this.state.selectedSquare) {
320
+ if (this.state.validMoves.includes(cursorSquare)) {
321
+ const move = this.game.makeMove(this.state.selectedSquare, cursorSquare);
322
+ if (move) {
323
+ this.state.lastMove = { from: this.state.selectedSquare, to: cursorSquare };
324
+ this.networkCallbacks.onMove(this.state.selectedSquare, cursorSquare);
325
+ this.state.isMyTurn = false;
326
+ if (this.game.isGameOver()) {
327
+ this.state.gameOver = true;
328
+ this.state.gameOverReason = this.game.getGameOverReason();
329
+ }
330
+ }
331
+ this.cancelSelection();
332
+ } else {
333
+ const piece = this.game.getPiece(cursorSquare);
334
+ if (piece && piece.color === this.state.playerColor) {
335
+ this.selectPiece(cursorSquare);
336
+ } else {
337
+ this.cancelSelection();
338
+ }
339
+ }
340
+ } else {
341
+ const piece = this.game.getPiece(cursorSquare);
342
+ if (piece && piece.color === this.state.playerColor) {
343
+ this.selectPiece(cursorSquare);
344
+ }
345
+ }
346
+ }
347
+ selectPiece(square) {
348
+ this.state.selectedSquare = square;
349
+ this.state.validMoves = this.game.getValidMoves(square);
350
+ }
351
+ cancelSelection() {
352
+ this.state.selectedSquare = null;
353
+ this.state.validMoves = [];
354
+ }
355
+ // Called when opponent makes a move
356
+ receiveMove(from, to) {
357
+ this.game.loadMove(from, to);
358
+ this.state.lastMove = { from, to };
359
+ this.state.isMyTurn = true;
360
+ if (this.game.isGameOver()) {
361
+ this.state.gameOver = true;
362
+ this.state.gameOverReason = this.game.getGameOverReason();
363
+ }
364
+ this.render();
365
+ }
366
+ // Called when opponent resigns
367
+ opponentResigned() {
368
+ this.state.gameOver = true;
369
+ this.state.gameOverReason = "Opponent resigned";
370
+ this.render();
371
+ }
372
+ render() {
373
+ console.clear();
374
+ console.log(chalk2.bold.cyan("\n \u2654 SCHESS - Chess over LAN \u265A\n"));
375
+ console.log(renderBoard(this.game, this.state));
376
+ console.log();
377
+ console.log(renderStatus(this.game, this.state));
378
+ console.log(renderControls());
379
+ console.log();
380
+ }
381
+ start() {
382
+ this.keyboard.start();
383
+ this.render();
384
+ }
385
+ stop() {
386
+ this.keyboard.stop();
387
+ }
388
+ };
389
+
390
+ // src/network/Server.ts
391
+ async function startServer() {
392
+ const port = getRandomPort();
393
+ const ip = getLanIp() || "localhost";
394
+ const wss = new WebSocketServer({ port });
395
+ let opponent = null;
396
+ let gameScreen = null;
397
+ console.log(chalk3.bold.cyan("\n \u2654 SCHESS - Chess over LAN \u265A\n"));
398
+ console.log(chalk3.green(" Game created! You are playing as White.\n"));
399
+ console.log(chalk3.white(" Share this command with your opponent:\n"));
400
+ console.log(chalk3.yellow.bold(` schess join ${ip}:${port}
401
+ `));
402
+ const spinner = ora("Waiting for opponent to join...").start();
403
+ wss.on("connection", (ws) => {
404
+ if (opponent) {
405
+ ws.send(serializeMessage({ type: "error", message: "Game is full" }));
406
+ ws.close();
407
+ return;
408
+ }
409
+ opponent = ws;
410
+ spinner.succeed("Opponent connected!");
411
+ ws.send(serializeMessage({ type: "start", color: "b" }));
412
+ gameScreen = new GameScreen("w", {
413
+ onMove: (from, to) => {
414
+ if (opponent && opponent.readyState === WebSocket.OPEN) {
415
+ opponent.send(serializeMessage({ type: "move", from, to }));
416
+ }
417
+ },
418
+ onResign: () => {
419
+ if (opponent && opponent.readyState === WebSocket.OPEN) {
420
+ opponent.send(serializeMessage({ type: "resign" }));
421
+ }
422
+ cleanup();
423
+ }
424
+ });
425
+ setTimeout(() => {
426
+ gameScreen?.start();
427
+ }, 500);
428
+ ws.on("message", (data) => {
429
+ const msg = parseMessage(data.toString());
430
+ if (!msg) return;
431
+ handleMessage(msg);
432
+ });
433
+ ws.on("close", () => {
434
+ if (gameScreen) {
435
+ gameScreen.opponentResigned();
436
+ setTimeout(() => {
437
+ cleanup();
438
+ process.exit(0);
439
+ }, 3e3);
440
+ }
441
+ });
442
+ ws.on("error", (err) => {
443
+ console.error(chalk3.red("Connection error:"), err.message);
444
+ });
445
+ });
446
+ function handleMessage(msg) {
447
+ switch (msg.type) {
448
+ case "move":
449
+ gameScreen?.receiveMove(msg.from, msg.to);
450
+ break;
451
+ case "resign":
452
+ gameScreen?.opponentResigned();
453
+ setTimeout(() => {
454
+ cleanup();
455
+ process.exit(0);
456
+ }, 3e3);
457
+ break;
458
+ }
459
+ }
460
+ function cleanup() {
461
+ gameScreen?.stop();
462
+ wss.close();
463
+ }
464
+ process.on("SIGINT", () => {
465
+ cleanup();
466
+ process.exit(0);
467
+ });
468
+ }
469
+
470
+ // src/commands/init.ts
471
+ async function initCommand() {
472
+ await startServer();
473
+ }
474
+
475
+ // src/network/Client.ts
476
+ import WebSocket2 from "ws";
477
+ import chalk4 from "chalk";
478
+ import ora2 from "ora";
479
+ async function connectToServer(address) {
480
+ const [host, portStr] = address.split(":");
481
+ if (!host || !portStr) {
482
+ console.error(chalk4.red("Invalid address format. Use: schess join <ip:port>"));
483
+ process.exit(1);
484
+ }
485
+ const port = parseInt(portStr, 10);
486
+ if (isNaN(port)) {
487
+ console.error(chalk4.red("Invalid port number"));
488
+ process.exit(1);
489
+ }
490
+ console.log(chalk4.bold.cyan("\n \u2654 SCHESS - Chess over LAN \u265A\n"));
491
+ const spinner = ora2(`Connecting to ${host}:${port}...`).start();
492
+ const ws = new WebSocket2(`ws://${host}:${port}`);
493
+ let gameScreen = null;
494
+ ws.on("open", () => {
495
+ spinner.succeed("Connected to game!");
496
+ ws.send(serializeMessage({ type: "join" }));
497
+ });
498
+ ws.on("message", (data) => {
499
+ const msg = parseMessage(data.toString());
500
+ if (!msg) return;
501
+ handleMessage(msg);
502
+ });
503
+ ws.on("close", () => {
504
+ if (gameScreen) {
505
+ gameScreen.opponentResigned();
506
+ setTimeout(() => {
507
+ cleanup();
508
+ process.exit(0);
509
+ }, 3e3);
510
+ } else {
511
+ spinner.fail("Disconnected from server");
512
+ process.exit(1);
513
+ }
514
+ });
515
+ ws.on("error", (err) => {
516
+ spinner.fail(`Connection failed: ${err.message}`);
517
+ process.exit(1);
518
+ });
519
+ function handleMessage(msg) {
520
+ switch (msg.type) {
521
+ case "start": {
522
+ const color = msg.color;
523
+ console.log(chalk4.green(` You are playing as ${color === "w" ? "White" : "Black"}.
524
+ `));
525
+ gameScreen = new GameScreen(color, {
526
+ onMove: (from, to) => {
527
+ if (ws.readyState === WebSocket2.OPEN) {
528
+ ws.send(serializeMessage({ type: "move", from, to }));
529
+ }
530
+ },
531
+ onResign: () => {
532
+ if (ws.readyState === WebSocket2.OPEN) {
533
+ ws.send(serializeMessage({ type: "resign" }));
534
+ }
535
+ cleanup();
536
+ }
537
+ });
538
+ setTimeout(() => {
539
+ gameScreen?.start();
540
+ }, 500);
541
+ break;
542
+ }
543
+ case "move":
544
+ gameScreen?.receiveMove(msg.from, msg.to);
545
+ break;
546
+ case "resign":
547
+ gameScreen?.opponentResigned();
548
+ setTimeout(() => {
549
+ cleanup();
550
+ process.exit(0);
551
+ }, 3e3);
552
+ break;
553
+ case "error":
554
+ console.error(chalk4.red(`Server error: ${msg.message}`));
555
+ cleanup();
556
+ process.exit(1);
557
+ break;
558
+ }
559
+ }
560
+ function cleanup() {
561
+ gameScreen?.stop();
562
+ ws.close();
563
+ }
564
+ process.on("SIGINT", () => {
565
+ cleanup();
566
+ process.exit(0);
567
+ });
568
+ }
569
+
570
+ // src/commands/join.ts
571
+ async function joinCommand(address) {
572
+ await connectToServer(address);
573
+ }
574
+
575
+ // src/index.ts
576
+ var program = new Command();
577
+ program.name("schess").description("CLI Chess game over LAN").version("1.0.0");
578
+ program.command("init").description("Host a new chess game").action(async () => {
579
+ await initCommand();
580
+ });
581
+ program.command("join <address>").description("Join a chess game (e.g., schess join 192.168.1.100:12345)").action(async (address) => {
582
+ await joinCommand(address);
583
+ });
584
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "schess",
3
+ "version": "1.0.0",
4
+ "description": "CLI Chess game over LAN",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "schess": "./bin/schess.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsup src/index.ts --format esm --dts --clean",
12
+ "dev": "tsup src/index.ts --format esm --watch",
13
+ "schess": "node ./bin/schess.js"
14
+ },
15
+ "keywords": [
16
+ "chess",
17
+ "cli",
18
+ "lan",
19
+ "multiplayer",
20
+ "game"
21
+ ],
22
+ "license": "MIT",
23
+ "dependencies": {
24
+ "chalk": "^5.3.0",
25
+ "chess.js": "^1.0.0-beta.8",
26
+ "commander": "^12.0.0",
27
+ "ora": "^8.0.1",
28
+ "ws": "^8.16.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^20.0.0",
32
+ "@types/ws": "^8.5.10",
33
+ "tsup": "^8.0.0",
34
+ "typescript": "^5.4.0"
35
+ },
36
+ "engines": {
37
+ "node": ">=18.0.0"
38
+ }
39
+ }
@@ -0,0 +1,5 @@
1
+ import { startServer } from '../network/Server.js';
2
+
3
+ export async function initCommand(): Promise<void> {
4
+ await startServer();
5
+ }
@@ -0,0 +1,5 @@
1
+ import { connectToServer } from '../network/Client.js';
2
+
3
+ export async function joinCommand(address: string): Promise<void> {
4
+ await connectToServer(address);
5
+ }