(schism): started separating backend from frontend
This commit is contained in:
parent
117b535962
commit
fb49ba4ee9
13 changed files with 1214 additions and 163 deletions
21
src/tic-tac-toe/package.json
Normal file
21
src/tic-tac-toe/package.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "tic-tac-toe",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "npm run build && node dist/run.js",
|
||||
"build": "vite build",
|
||||
"build:prod": "vite build --outDir=/dist --minify=true --sourcemap=false",
|
||||
"REMOVEME-build:openapi": "VITE_ENTRYPOINT=src/openapi.ts vite build && node dist/openapi.cjs >openapi.json",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"packageManager": "pnpm@10.24.0",
|
||||
"dependencies": {
|
||||
"fastify-socket.io": "^5.1.0",
|
||||
"socket.io": "^4.8.1"
|
||||
}
|
||||
}
|
||||
240
src/tic-tac-toe/src/app.ts
Normal file
240
src/tic-tac-toe/src/app.ts
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
import { FastifyInstance, FastifyPluginAsync } from 'fastify';
|
||||
import fastifySocketIO from 'fastify-socket.io';
|
||||
import { TTC } from './game';
|
||||
|
||||
const app: FastifyPluginAsync = async (fastify: FastifyInstance, opts): Promise<void> => {
|
||||
void opts;
|
||||
await fastify.register(fastifySocketIO, {
|
||||
cors: {
|
||||
origin: '*',
|
||||
methods: ['GET', 'POST'],
|
||||
},
|
||||
});
|
||||
|
||||
const game = new TTC();
|
||||
|
||||
fastify.ready().then(() => {
|
||||
fastify.io.on('connection', (socket) => {
|
||||
fastify.log.info(`Client connected: ${socket.id}`);
|
||||
|
||||
socket.emit('gameState', {
|
||||
board: game.board,
|
||||
turn: game.currentPlayer,
|
||||
gameOver: game.isGameOver,
|
||||
});
|
||||
|
||||
socket.on('makeMove', (idx: number) => {
|
||||
const result = game.makeMove(idx);
|
||||
|
||||
if (result === 'invalidMove') {
|
||||
socket.emit('error', 'Invalid Move');
|
||||
}
|
||||
else {
|
||||
fastify.io.emit('gameState', {
|
||||
board: game.board,
|
||||
turn: game.currentPlayer,
|
||||
lastResult: result,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('resetGame', () => {
|
||||
game.reset();
|
||||
fastify.io.emit('gameState', {
|
||||
board: game.board,
|
||||
turn: game.currentPlayer,
|
||||
reset: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default app;
|
||||
|
||||
// // TODO: Import the plugins defined for this microservice
|
||||
// // TODO: Import the routes defined for this microservice
|
||||
|
||||
// // @brief The microservice app (as a plugin for Fastify), kinda like a main function I guess ???
|
||||
// // @param fastify
|
||||
// // @param opts
|
||||
// export const app: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
||||
// // Register all the fastify plugins that this app will use
|
||||
|
||||
// // Once it is done:
|
||||
// fastify.ready((err) => {
|
||||
// if (err) {
|
||||
// throw err;
|
||||
// }
|
||||
// // TODO: Supposedly, something should be there I guess
|
||||
// });
|
||||
// };
|
||||
// // Export it as the default for this file.
|
||||
// export default app;
|
||||
|
||||
// // TODO: Understand what is this for in /src/chat/src/app.ts
|
||||
// // declare module 'fastify' {
|
||||
// // interface FastifyInstance {
|
||||
// // io: Server<{
|
||||
// // hello: (message: string) => string;
|
||||
// // MsgObjectServer: (data: { message: ClientMessage }) => void;
|
||||
// // message: (msg: string) => void;
|
||||
// // testend: (sock_id_client: string) => void;
|
||||
// // }>;
|
||||
// // }
|
||||
// // }
|
||||
|
||||
// // TODO: Same for this, also in /src/chat/src/app.ts
|
||||
// // async function onReady(fastify: FastifyInstance) {
|
||||
// // function connectedUser(io?: Server, target?: string): number {
|
||||
// // let count = 0;
|
||||
// // const seen = new Set<string>();
|
||||
// // // <- only log/count unique usernames
|
||||
|
||||
// // for (const [socketId, username] of clientChat) {
|
||||
// // // Basic sanity checks
|
||||
// // if (typeof socketId !== 'string' || socketId.length === 0) {
|
||||
// // clientChat.delete(socketId);
|
||||
// // continue;
|
||||
// // }
|
||||
// // if (typeof username !== 'string' || username.length === 0) {
|
||||
// // clientChat.delete(socketId);
|
||||
// // continue;
|
||||
// // }
|
||||
|
||||
// // // If we have the io instance, attempt to validate the socket is still connected
|
||||
// // if (io && typeof io.sockets?.sockets?.get === 'function') {
|
||||
// // const s = io.sockets.sockets.get(socketId) as
|
||||
// // | Socket
|
||||
// // | undefined;
|
||||
// // // If socket not found or disconnected, remove from map and skip
|
||||
// // if (!s || s.disconnected) {
|
||||
// // clientChat.delete(socketId);
|
||||
// // continue;
|
||||
// // }
|
||||
|
||||
// // // Skip duplicates (DO NOT delete them — just don't count)
|
||||
// // if (seen.has(username)) {
|
||||
// // continue;
|
||||
// // }
|
||||
// // // socket exists and is connected
|
||||
// // seen.add(username);
|
||||
// // count++;
|
||||
// // // console.log(color.green,"count: ", count);
|
||||
// // console.log(color.yellow, 'Client:', color.reset, username);
|
||||
|
||||
// // const targetSocketId = target;
|
||||
// // io.to(targetSocketId!).emit('listObj', username);
|
||||
|
||||
// // console.log(
|
||||
// // color.yellow,
|
||||
// // 'Chat Socket ID:',
|
||||
// // color.reset,
|
||||
// // socketId,
|
||||
// // );
|
||||
// // continue;
|
||||
// // }
|
||||
|
||||
// // // If no io provided, assume entries in the map are valid and count them.
|
||||
// // count++;
|
||||
// // console.log(
|
||||
// // color.red,
|
||||
// // 'Client (unverified):',
|
||||
// // color.reset,
|
||||
// // username,
|
||||
// // );
|
||||
// // console.log(
|
||||
// // color.red,
|
||||
// // 'Chat Socket ID (unverified):',
|
||||
// // color.reset,
|
||||
// // socketId,
|
||||
// // );
|
||||
// // }
|
||||
|
||||
// // return count;
|
||||
// // }
|
||||
|
||||
// // function broadcast(data: ClientMessage, sender?: string) {
|
||||
// // fastify.io.fetchSockets().then((sockets) => {
|
||||
// // for (const s of sockets) {
|
||||
// // if (s.id !== sender) {
|
||||
// // // Send REAL JSON object
|
||||
// // const clientName = clientChat.get(s.id) || null;
|
||||
// // if (clientName !== null) {
|
||||
// // s.emit('MsgObjectServer', { message: data });
|
||||
// // }
|
||||
// // console.log(' Target window socket ID:', s.id);
|
||||
// // console.log(' Target window ID:', [...s.rooms]);
|
||||
// // console.log(' Sender window ID:', sender ? sender : 'none');
|
||||
// // }
|
||||
// // }
|
||||
// // });
|
||||
// // }
|
||||
|
||||
// // fastify.io.on('connection', (socket: Socket) => {
|
||||
// // socket.on('message', (message: string) => {
|
||||
// // console.info(
|
||||
// // color.blue,
|
||||
// // 'Socket connected!',
|
||||
// // color.reset,
|
||||
// // socket.id,
|
||||
// // );
|
||||
// // console.log(
|
||||
// // color.blue,
|
||||
// // 'Received message from client',
|
||||
// // color.reset,
|
||||
// // message,
|
||||
// // );
|
||||
|
||||
// // const obj: ClientMessage = JSON.parse(message) as ClientMessage;
|
||||
// // clientChat.set(socket.id, obj.user);
|
||||
// // console.log(
|
||||
// // color.green,
|
||||
// // 'Message from client',
|
||||
// // color.reset,
|
||||
// // `Sender: login name: "${obj.user}" - windowID "${obj.SenderWindowID}" - text message: "${obj.text}"`,
|
||||
// // );
|
||||
// // // Send object directly — DO NOT wrap it in a string
|
||||
// // broadcast(obj, obj.SenderWindowID);
|
||||
// // console.log(
|
||||
// // color.red,
|
||||
// // 'connected in the Chat :',
|
||||
// // connectedUser(fastify.io),
|
||||
// // color.reset,
|
||||
// // );
|
||||
// // });
|
||||
|
||||
// // socket.on('testend', (sock_id_cl: string) => {
|
||||
// // console.log('testend received from client socket id:', sock_id_cl);
|
||||
// // });
|
||||
|
||||
// // socket.on('list', () => {
|
||||
// // console.log(color.red, 'list activated', color.reset, socket.id);
|
||||
// // connectedUser(fastify.io, socket.id);
|
||||
// // });
|
||||
|
||||
// // socket.on('disconnecting', (reason) => {
|
||||
// // const clientName = clientChat.get(socket.id) || null;
|
||||
// // console.log(
|
||||
// // color.green,
|
||||
// // `Client disconnecting: ${clientName} (${socket.id}) reason:`,
|
||||
// // reason,
|
||||
// // );
|
||||
// // if (reason === 'transport error') return;
|
||||
|
||||
// // if (clientName !== null) {
|
||||
// // const obj = {
|
||||
// // type: 'chat',
|
||||
// // user: clientName,
|
||||
// // token: '',
|
||||
// // text: 'LEFT the chat',
|
||||
// // timestamp: Date.now(),
|
||||
// // SenderWindowID: socket.id,
|
||||
// // };
|
||||
|
||||
// // broadcast(obj, obj.SenderWindowID);
|
||||
// // // clientChat.delete(obj.user);
|
||||
// // }
|
||||
// // });
|
||||
// // });
|
||||
// // }
|
||||
81
src/tic-tac-toe/src/game.ts
Normal file
81
src/tic-tac-toe/src/game.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
// Represents the possible states of a cell on the board.
|
||||
// `null` means that the cell is empty.
|
||||
type CellState = 'O' | 'X' | null
|
||||
|
||||
export class TTC {
|
||||
private isGameOver: boolean = false;
|
||||
public board: CellState[] = Array(9).fill(null);
|
||||
private currentPlayer: 'O' | 'X' = 'X';
|
||||
|
||||
private changePlayer() {
|
||||
this.currentPlayer = this.currentPlayer === 'X' ? 'O' : 'X';
|
||||
}
|
||||
|
||||
// Analyzes the current board to determine if the game has ended.
|
||||
private checkState(): 'winX' | 'winO' | 'draw' | 'ongoing' {
|
||||
const checkRow = (row: number): ('X' | 'O' | null) => {
|
||||
if (this.board[row * 3] === null) {return null;}
|
||||
if (this.board[row * 3] === this.board[row * 3 + 1] && this.board[row * 3 + 1] === this.board[row * 3 + 2]) {return this.board[row * 3];}
|
||||
return null;
|
||||
};
|
||||
|
||||
const checkCol = (col: number): ('X' | 'O' | null) => {
|
||||
if (this.board[col] === null) return null;
|
||||
|
||||
if (this.board[col] === this.board[col + 3] && this.board[col + 3] === this.board[col + 6]) {return this.board[col];}
|
||||
return null;
|
||||
};
|
||||
|
||||
const checkDiag = (): ('X' | 'O' | null) => {
|
||||
if (this.board[4] === null) return null;
|
||||
|
||||
if (this.board[0] === this.board[4] && this.board[4] === this.board[8]) {return this.board[4];}
|
||||
|
||||
if (this.board[2] === this.board[4] && this.board[4] === this.board[6]) {return this.board[4];}
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
const row = (checkRow(0) ?? checkRow(1)) ?? checkRow(2);
|
||||
const col = (checkCol(0) ?? checkCol(1)) ?? checkCol(2);
|
||||
const diag = checkDiag();
|
||||
|
||||
if (row !== null) return `win${row}`;
|
||||
if (col !== null) return `win${col}`;
|
||||
if (diag !== null) return `win${diag}`;
|
||||
|
||||
if (this.board.filter(c => c === null).length === 0) {return 'draw';}
|
||||
return 'ongoing';
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.board = [null, null, null, null, null, null, null, null, null];
|
||||
this.currentPlayer = 'X';
|
||||
this.isGameOver = false;
|
||||
};
|
||||
|
||||
// Attempts to place the current player's mark on the specified cell.
|
||||
// @param idx - The index of the board (0-8) to place the mark.
|
||||
// @returns The resulting game state, or `invalidMove` if the move is illegal.
|
||||
public makeMove(idx: number): 'winX' | 'winO' | 'draw' | 'ongoing' | 'invalidMove' {
|
||||
if (this.isGameOver) {
|
||||
return 'invalidMove';
|
||||
}
|
||||
if (idx < 0 || idx >= this.board.length) {
|
||||
return 'invalidMove';
|
||||
}
|
||||
if (this.board[idx] !== null) {
|
||||
return 'invalidMove';
|
||||
}
|
||||
this.board[idx] = this.currentPlayer;
|
||||
this.changePlayer();
|
||||
|
||||
const result = this.checkState();
|
||||
|
||||
if (result !== 'ongoing') {
|
||||
this.isGameOver = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue