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.
@@ -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
+ }
@@ -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 };