Compare commits
37 Commits
main
...
landing-pa
| Author | SHA1 | Date |
|---|---|---|
|
|
00f22aee28 | |
|
|
e7ea4269d7 | |
|
|
1d777f74ff | |
|
|
e982f782cb | |
|
|
496c68550b | |
|
|
b878beaaff | |
|
|
eff695837b | |
|
|
c0c7a8468e | |
|
|
5fe216dbc0 | |
|
|
9fbfe40f7f | |
|
|
8a4593b542 | |
|
|
10269ec448 | |
|
|
7391d78c03 | |
|
|
8bbfe472c6 | |
|
|
f04f153a0e | |
|
|
44cb010faf | |
|
|
0650f0481b | |
|
|
b7561d804c | |
|
|
949969bda3 | |
|
|
b692caf93d | |
|
|
4db2aa7407 | |
|
|
3525d62c19 | |
|
|
bd6d330226 | |
|
|
6fab2d94a0 | |
|
|
6bb0e1aa74 | |
|
|
87d61278a1 | |
|
|
7a5125ea38 | |
|
|
d90942624b | |
|
|
37a84be3c3 | |
|
|
9f5ec017b7 | |
|
|
47746b5009 | |
|
|
e2bf337b25 | |
|
|
5d55631812 | |
|
|
e323623007 | |
|
|
3a9b88ef55 | |
|
|
d9eafeb71a | |
|
|
74eadacdb2 |
|
|
@ -16,7 +16,8 @@
|
||||||
"@nestjs/websockets": "^10.4.7",
|
"@nestjs/websockets": "^10.4.7",
|
||||||
"interface": "file:../interface",
|
"interface": "file:../interface",
|
||||||
"reflect-metadata": "^0.2.0",
|
"reflect-metadata": "^0.2.0",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1",
|
||||||
|
"uuid": "^11.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.0.0",
|
"@nestjs/cli": "^10.0.0",
|
||||||
|
|
@ -8948,6 +8949,19 @@
|
||||||
"node": ">= 0.4.0"
|
"node": ">= 0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "11.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz",
|
||||||
|
"integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/esm/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/v8-compile-cache-lib": {
|
"node_modules/v8-compile-cache-lib": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,8 @@
|
||||||
"@nestjs/websockets": "^10.4.7",
|
"@nestjs/websockets": "^10.4.7",
|
||||||
"interface": "file:../interface",
|
"interface": "file:../interface",
|
||||||
"reflect-metadata": "^0.2.0",
|
"reflect-metadata": "^0.2.0",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1",
|
||||||
|
"uuid": "^11.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.0.0",
|
"@nestjs/cli": "^10.0.0",
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import { PlayerService } from './players/player.service';
|
import { PlayerService } from './players/player.service';
|
||||||
|
import { GameService } from './games/game.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [],
|
||||||
providers: [AppService, PlayerService],
|
providers: [AppService, PlayerService, GameService],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger, UseFilters } from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
ConnectedSocket,
|
ConnectedSocket,
|
||||||
OnGatewayConnection,
|
OnGatewayConnection,
|
||||||
|
|
@ -7,35 +7,81 @@ import {
|
||||||
WebSocketGateway,
|
WebSocketGateway,
|
||||||
} from '@nestjs/websockets';
|
} from '@nestjs/websockets';
|
||||||
import { PlayerService } from './players/player.service';
|
import { PlayerService } from './players/player.service';
|
||||||
|
import { GameService } from 'src/games/game.service';
|
||||||
import { Socket } from 'socket.io';
|
import { Socket } from 'socket.io';
|
||||||
import { ClientEvent, CreateLobbyEvent, JoinLobbyEvent } from 'interface';
|
import {
|
||||||
|
ClientEvent,
|
||||||
|
CreateLobbyEvent,
|
||||||
|
emitUpdateLobbyEvent,
|
||||||
|
JoinLobbyEvent,
|
||||||
|
} from 'interface';
|
||||||
|
import { createWsExceptionFilter } from './websocket-exception-filter';
|
||||||
|
import {
|
||||||
|
GameNotFoundException,
|
||||||
|
InvalidPlayerNameException,
|
||||||
|
MissingPlayerNameException,
|
||||||
|
PlayerNotFoundException,
|
||||||
|
} from './exceptions';
|
||||||
|
|
||||||
@WebSocketGateway({ cors: true })
|
@WebSocketGateway({ cors: true })
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AppService implements OnGatewayConnection {
|
export class AppService implements OnGatewayConnection {
|
||||||
private readonly logger = new Logger(AppService.name);
|
private readonly logger = new Logger(AppService.name);
|
||||||
|
|
||||||
constructor(private readonly playerService: PlayerService) {}
|
constructor(
|
||||||
|
private readonly playerService: PlayerService,
|
||||||
|
private readonly gameService: GameService,
|
||||||
|
) {}
|
||||||
handleConnection(client: Socket) {
|
handleConnection(client: Socket) {
|
||||||
this.playerService.createPlayer(client.id);
|
this.playerService.createPlayer(client);
|
||||||
this.logger.log(client.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseFilters(
|
||||||
|
createWsExceptionFilter([
|
||||||
|
PlayerNotFoundException,
|
||||||
|
MissingPlayerNameException,
|
||||||
|
InvalidPlayerNameException,
|
||||||
|
]),
|
||||||
|
)
|
||||||
@SubscribeMessage(ClientEvent.CREATE_LOBBY)
|
@SubscribeMessage(ClientEvent.CREATE_LOBBY)
|
||||||
handleCreateLobby(
|
handleCreateLobby(
|
||||||
@ConnectedSocket() client: Socket,
|
@ConnectedSocket() client: Socket,
|
||||||
@MessageBody() event: CreateLobbyEvent,
|
@MessageBody() event: CreateLobbyEvent,
|
||||||
) {
|
) {
|
||||||
this.playerService.addUserName(client.id, event.userName);
|
this.playerService.addUserName(client.id, event.userName);
|
||||||
this.logger.log('Se ha creado un lobby');
|
const game = this.gameService.createLobby(
|
||||||
|
this.playerService.getPlayer(client.id),
|
||||||
|
);
|
||||||
|
emitUpdateLobbyEvent(client, {
|
||||||
|
playerNames: game.players.map((player) => player.userName),
|
||||||
|
gameCode: game.gameCode,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseFilters(
|
||||||
|
createWsExceptionFilter([
|
||||||
|
PlayerNotFoundException,
|
||||||
|
MissingPlayerNameException,
|
||||||
|
InvalidPlayerNameException,
|
||||||
|
GameNotFoundException,
|
||||||
|
]),
|
||||||
|
)
|
||||||
@SubscribeMessage(ClientEvent.JOIN_LOBBY)
|
@SubscribeMessage(ClientEvent.JOIN_LOBBY)
|
||||||
handleJoinLobby(
|
handleJoinLobby(
|
||||||
@ConnectedSocket() client: Socket,
|
@ConnectedSocket() client: Socket,
|
||||||
@MessageBody() event: JoinLobbyEvent,
|
@MessageBody() event: JoinLobbyEvent,
|
||||||
) {
|
) {
|
||||||
this.playerService.addUserName(client.id, event.userName);
|
this.playerService.addUserName(client.id, event.userName);
|
||||||
this.logger.log('Te has unido a un lobby');
|
const game = this.gameService.joinGame(
|
||||||
|
this.playerService.getPlayer(client.id),
|
||||||
|
event.lobbyId,
|
||||||
|
);
|
||||||
|
const playerNames = game.players.map((player) => player.userName);
|
||||||
|
game.players.forEach((player) =>
|
||||||
|
emitUpdateLobbyEvent(player.socket, {
|
||||||
|
playerNames: playerNames,
|
||||||
|
gameCode: game.gameCode,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { WsException } from '@nestjs/websockets';
|
||||||
|
import { ErrorCode } from 'interface';
|
||||||
|
|
||||||
|
export abstract class WebSocketException extends WsException {
|
||||||
|
public readonly errorCode: ErrorCode;
|
||||||
|
|
||||||
|
constructor(message: string, errorCode: ErrorCode) {
|
||||||
|
super(message);
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MissingPlayerNameException extends WebSocketException {
|
||||||
|
constructor() {
|
||||||
|
super('Missing player name', ErrorCode.MISSING_USER_NAME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InvalidPlayerNameException extends WebSocketException {
|
||||||
|
constructor(userName: string) {
|
||||||
|
super('Invalid player name: ' + userName, ErrorCode.INVALID_USER_NAME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PlayerNotFoundException extends WebSocketException {
|
||||||
|
constructor(playerId: string) {
|
||||||
|
super(
|
||||||
|
'Unable to find player from ID: ' + playerId,
|
||||||
|
ErrorCode.PLAYER_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GameNotFoundException extends WebSocketException {
|
||||||
|
constructor(gameId: string) {
|
||||||
|
super('Unable to find game from ID: ' + gameId, ErrorCode.GAME_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { Game } from './game';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { Player } from 'src/players/player';
|
||||||
|
import { GameNotFoundException } from 'src/exceptions';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GameService {
|
||||||
|
private readonly logger = new Logger(GameService.name);
|
||||||
|
private readonly games: Map<string, Game> = new Map();
|
||||||
|
|
||||||
|
createLobby(player: Player): Game {
|
||||||
|
const gameId = uuidv4();
|
||||||
|
const gameCode = uuidv4().slice(-5).toUpperCase();
|
||||||
|
const game: Game = new Game(gameId, gameCode, player);
|
||||||
|
this.games.set(gameCode, game);
|
||||||
|
return game;
|
||||||
|
}
|
||||||
|
|
||||||
|
joinGame(player: Player, gameCode: string): Game {
|
||||||
|
const game = this.games.get(gameCode);
|
||||||
|
if (game === undefined) {
|
||||||
|
throw new GameNotFoundException(gameCode);
|
||||||
|
}
|
||||||
|
game.players.push(player);
|
||||||
|
return game;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Player } from 'src/players/player';
|
||||||
|
|
||||||
|
export class Game {
|
||||||
|
gameId: string;
|
||||||
|
gameCode: string;
|
||||||
|
players: Player[];
|
||||||
|
constructor(gameId: string, gameCode: string, player: Player) {
|
||||||
|
this.gameId = gameId;
|
||||||
|
this.gameCode = gameCode;
|
||||||
|
this.players = [player];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,38 @@
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { Player } from './player';
|
import { Player } from './player';
|
||||||
|
import {
|
||||||
|
InvalidPlayerNameException,
|
||||||
|
MissingPlayerNameException,
|
||||||
|
PlayerNotFoundException,
|
||||||
|
} from 'src/exceptions';
|
||||||
|
import { Socket } from 'socket.io/dist/socket';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PlayerService {
|
export class PlayerService {
|
||||||
private readonly logger = new Logger(PlayerService.name);
|
private readonly logger = new Logger(PlayerService.name);
|
||||||
private readonly players: Map<string, Player> = new Map();
|
private readonly players: Map<string, Player> = new Map();
|
||||||
|
private readonly userNameValidator: RegExp = /^[a-zA-Z\s]{1,20}$/;
|
||||||
|
|
||||||
createPlayer(socketId: string) {
|
createPlayer(socket: Socket) {
|
||||||
const player: Player = new Player(socketId);
|
const player: Player = new Player(socket);
|
||||||
this.players.set(socketId, player);
|
this.players.set(player.socketId, player);
|
||||||
this.logger.log([...this.players.entries()]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addUserName(socketId: string, userName: string) {
|
addUserName(socketId: string, userName: string) {
|
||||||
|
if (!userName) {
|
||||||
|
throw new MissingPlayerNameException();
|
||||||
|
}
|
||||||
|
if (!this.userNameValidator.test(userName)) {
|
||||||
|
throw new InvalidPlayerNameException(userName);
|
||||||
|
}
|
||||||
this.players.get(socketId).userName = userName;
|
this.players.get(socketId).userName = userName;
|
||||||
this.logger.log([...this.players.entries()]);
|
}
|
||||||
|
|
||||||
|
getPlayer(socketId: string): Player {
|
||||||
|
const player = this.players.get(socketId);
|
||||||
|
if (!player) {
|
||||||
|
throw new PlayerNotFoundException(socketId);
|
||||||
|
}
|
||||||
|
return player;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
|
import { Socket } from 'socket.io';
|
||||||
|
|
||||||
export class Player {
|
export class Player {
|
||||||
socketId: string;
|
socketId: string;
|
||||||
|
socket: Socket;
|
||||||
userName?: string;
|
userName?: string;
|
||||||
constructor(socketId: string) {
|
|
||||||
this.socketId = socketId;
|
constructor(socket: Socket) {
|
||||||
|
this.socket = socket;
|
||||||
|
this.socketId = socket.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { ArgumentsHost, Catch, Logger } from '@nestjs/common';
|
||||||
|
import { BaseWsExceptionFilter } from '@nestjs/websockets';
|
||||||
|
import { WebSocketException } from './exceptions';
|
||||||
|
import { Socket } from 'socket.io';
|
||||||
|
import { ClientEvent } from 'interface';
|
||||||
|
import { emitCreateLobbyError, emitJoinLobbyError } from 'interface';
|
||||||
|
|
||||||
|
export const createWsExceptionFilter: (
|
||||||
|
exceptionList: (typeof WebSocketException)[],
|
||||||
|
) => BaseWsExceptionFilter<WebSocketException> = (handledExceptions) => {
|
||||||
|
@Catch(...handledExceptions)
|
||||||
|
class WsExceptionFilter extends BaseWsExceptionFilter<WebSocketException> {
|
||||||
|
private readonly logger = new Logger(BaseWsExceptionFilter.name);
|
||||||
|
|
||||||
|
catch(exception: WebSocketException, host: ArgumentsHost) {
|
||||||
|
const socket = host.switchToWs().getClient() as Socket;
|
||||||
|
const method = host.switchToWs().getPattern();
|
||||||
|
const data: object = host.switchToWs().getData();
|
||||||
|
this.logger.log(
|
||||||
|
`Caught exception: ${exception}
|
||||||
|
for request to ${method}
|
||||||
|
from client ${socket.id}
|
||||||
|
with input ${JSON.stringify(data)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (method) {
|
||||||
|
case ClientEvent.CREATE_LOBBY:
|
||||||
|
emitCreateLobbyError(socket, { error: exception.errorCode });
|
||||||
|
break;
|
||||||
|
case ClientEvent.JOIN_LOBBY:
|
||||||
|
emitJoinLobbyError(socket, { error: exception.errorCode });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new WsExceptionFilter();
|
||||||
|
};
|
||||||
|
|
@ -13,5 +13,7 @@ module.exports = {
|
||||||
jest: true,
|
jest: true,
|
||||||
},
|
},
|
||||||
ignorePatterns: [".eslintrc.js", "dist/"],
|
ignorePatterns: [".eslintrc.js", "dist/"],
|
||||||
rules: {},
|
rules: {
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "off",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
import { CellType } from "./constants/CellType";
|
||||||
|
import { Direction } from "./constants/Direction";
|
||||||
|
import { ExitType } from "./constants/ExitType";
|
||||||
|
import { Cell } from "./types/Cell";
|
||||||
|
import { Exit } from "./types/Exit";
|
||||||
|
|
||||||
|
const boardSize = 7;
|
||||||
|
const universityLocations = [
|
||||||
|
[2, 0],
|
||||||
|
[4, 1],
|
||||||
|
[3, 3],
|
||||||
|
];
|
||||||
|
const factoryLocations = [
|
||||||
|
[0, 0],
|
||||||
|
[5, 2],
|
||||||
|
[4, 6],
|
||||||
|
];
|
||||||
|
const houseLocations = [
|
||||||
|
[0, 2],
|
||||||
|
[1, 5],
|
||||||
|
[2, 4],
|
||||||
|
[5, 4],
|
||||||
|
];
|
||||||
|
const mapPosition = (position: number[], type: CellType) => {
|
||||||
|
return {
|
||||||
|
row: position[0],
|
||||||
|
col: position[1],
|
||||||
|
type: type,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const specialUniversityCells = universityLocations.map((position) =>
|
||||||
|
mapPosition(position, CellType.UNIVERSITY),
|
||||||
|
);
|
||||||
|
const specialFactoryCells = factoryLocations.map((position) =>
|
||||||
|
mapPosition(position, CellType.FACTORY),
|
||||||
|
);
|
||||||
|
const specialHouseCells = houseLocations.map((position) =>
|
||||||
|
mapPosition(position, CellType.HOUSE),
|
||||||
|
);
|
||||||
|
const specialCells = specialUniversityCells
|
||||||
|
.concat(specialFactoryCells)
|
||||||
|
.concat(specialHouseCells);
|
||||||
|
|
||||||
|
const specialExitIndexes1 = [1, 5];
|
||||||
|
const specialExitIndexes2 = [3];
|
||||||
|
|
||||||
|
function createBoard(): Cell[][] {
|
||||||
|
const indexes = Array.from(Array(boardSize).keys());
|
||||||
|
return indexes.map((rowIndex) =>
|
||||||
|
indexes.map((colIndex) => {
|
||||||
|
const specialCell = specialCells.find(
|
||||||
|
(specialCell) =>
|
||||||
|
specialCell.row === rowIndex && specialCell.col === colIndex,
|
||||||
|
);
|
||||||
|
return new Cell(specialCell ? specialCell.type : CellType.NORMAL);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectAdjacentCells(board: Cell[][]) {
|
||||||
|
const indexes = Array.from(Array(boardSize).keys());
|
||||||
|
for (const rowIndex of indexes.slice(0, -1)) {
|
||||||
|
for (const colIndex of indexes) {
|
||||||
|
const cell = board[rowIndex][colIndex];
|
||||||
|
const bottomCell = board[rowIndex + 1][colIndex];
|
||||||
|
cell
|
||||||
|
.getNodeAt(Direction.SOUTH)
|
||||||
|
.linkToNode(bottomCell.getNodeAt(Direction.NORTH));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const rowIndex of indexes) {
|
||||||
|
for (const colIndex of indexes.slice(0, -1)) {
|
||||||
|
const cell = board[rowIndex][colIndex];
|
||||||
|
const rightCell = board[rowIndex][colIndex + 1];
|
||||||
|
cell
|
||||||
|
.getNodeAt(Direction.EAST)
|
||||||
|
.linkToNode(rightCell.getNodeAt(Direction.WEST));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addExits(board: Cell[][]) {
|
||||||
|
// Add exits to top row
|
||||||
|
board[0].forEach((cell, colIndex) => {
|
||||||
|
let exitType = ExitType.AMBIVALENT;
|
||||||
|
if (specialExitIndexes1.includes(colIndex)) exitType = ExitType.ROAD;
|
||||||
|
if (specialExitIndexes2.includes(colIndex)) exitType = ExitType.RAIL;
|
||||||
|
cell.getNodeAt(Direction.NORTH).linkToNode(new Exit(exitType));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add exits to bottom row
|
||||||
|
board[boardSize - 1].forEach((cell, colIndex) => {
|
||||||
|
let exitType = ExitType.AMBIVALENT;
|
||||||
|
if (specialExitIndexes1.includes(colIndex)) exitType = ExitType.ROAD;
|
||||||
|
if (specialExitIndexes2.includes(colIndex)) exitType = ExitType.RAIL;
|
||||||
|
cell.getNodeAt(Direction.SOUTH).linkToNode(new Exit(exitType));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add exits to left and right columns
|
||||||
|
board.forEach((row, rowIndex) => {
|
||||||
|
let exitType = ExitType.AMBIVALENT;
|
||||||
|
if (specialExitIndexes1.includes(rowIndex)) exitType = ExitType.RAIL;
|
||||||
|
if (specialExitIndexes2.includes(rowIndex)) exitType = ExitType.ROAD;
|
||||||
|
row[0].getNodeAt(Direction.WEST).linkToNode(new Exit(exitType));
|
||||||
|
row[boardSize - 1].getNodeAt(Direction.EAST).linkToNode(new Exit(exitType));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildBoard(): Cell[][] {
|
||||||
|
const board: Cell[][] = createBoard();
|
||||||
|
connectAdjacentCells(board);
|
||||||
|
addExits(board);
|
||||||
|
return board;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export enum CellType {
|
||||||
|
NORMAL = "NORMAL",
|
||||||
|
HOUSE = "HOUSE",
|
||||||
|
FACTORY = "FACTORY",
|
||||||
|
UNIVERSITY = "UNIVERSITY",
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
export enum Direction {
|
||||||
|
NORTH = "NORTH",
|
||||||
|
SOUTH = "SOUTH",
|
||||||
|
EAST = "EAST",
|
||||||
|
WEST = "WEST",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const directions: Direction[] = [
|
||||||
|
Direction.NORTH,
|
||||||
|
Direction.SOUTH,
|
||||||
|
Direction.EAST,
|
||||||
|
Direction.WEST,
|
||||||
|
];
|
||||||
|
|
||||||
|
export function rotateDirection(
|
||||||
|
initialDirection: Direction,
|
||||||
|
rotationAngle: 0 | 90 | 180 | 270,
|
||||||
|
): Direction {
|
||||||
|
const angleToDirectionMap: Record<Direction, 0 | 90 | 180 | 270> = {
|
||||||
|
[Direction.NORTH]: 0,
|
||||||
|
[Direction.EAST]: 90,
|
||||||
|
[Direction.SOUTH]: 180,
|
||||||
|
[Direction.WEST]: 270,
|
||||||
|
};
|
||||||
|
const finalAngle =
|
||||||
|
(360 + angleToDirectionMap[initialDirection] - rotationAngle) % 360;
|
||||||
|
return (Object.keys(angleToDirectionMap) as Direction[]).find(
|
||||||
|
(direction) => angleToDirectionMap[direction] === finalAngle,
|
||||||
|
)!;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
export enum ExitType {
|
||||||
|
RAIL = "RAIL",
|
||||||
|
ROAD = "ROAD",
|
||||||
|
AMBIVALENT = "AMBIVALENT",
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
export enum InternalNodeType {
|
||||||
|
NONE = "NONE",
|
||||||
|
STATION = "STATION",
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,410 @@
|
||||||
|
import { Piece } from "../types/Piece";
|
||||||
|
import { Direction } from "./Direction";
|
||||||
|
import { InternalNodeType } from "./InternalNodeType";
|
||||||
|
import { TrackType } from "./TrackType";
|
||||||
|
|
||||||
|
const STRAIGHT_RAIL: Piece = new Piece({
|
||||||
|
useInternalTracks: false,
|
||||||
|
trackDefinitions: [
|
||||||
|
{
|
||||||
|
startPoint: Direction.NORTH,
|
||||||
|
endPoint: Direction.SOUTH,
|
||||||
|
type: TrackType.RAIL,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const TURN_RAIL: Piece = new Piece({
|
||||||
|
useInternalTracks: false,
|
||||||
|
trackDefinitions: [
|
||||||
|
{
|
||||||
|
startPoint: Direction.SOUTH,
|
||||||
|
endPoint: Direction.EAST,
|
||||||
|
type: TrackType.RAIL,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const FOUR_WAY_CROSS_RAIL: Piece = new Piece({
|
||||||
|
useInternalTracks: true,
|
||||||
|
internalNodeType: InternalNodeType.NONE,
|
||||||
|
trackDefinitions: [
|
||||||
|
{
|
||||||
|
startPoint: Direction.NORTH,
|
||||||
|
type: TrackType.RAIL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.EAST,
|
||||||
|
type: TrackType.RAIL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.SOUTH,
|
||||||
|
type: TrackType.RAIL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.WEST,
|
||||||
|
type: TrackType.RAIL,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const T_JUNCTION_RAIL: Piece = new Piece({
|
||||||
|
useInternalTracks: true,
|
||||||
|
internalNodeType: InternalNodeType.NONE,
|
||||||
|
trackDefinitions: [
|
||||||
|
{
|
||||||
|
startPoint: Direction.WEST,
|
||||||
|
type: TrackType.RAIL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.SOUTH,
|
||||||
|
type: TrackType.RAIL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.EAST,
|
||||||
|
type: TrackType.RAIL,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const DEAD_END_STATION_RAIL: Piece = new Piece({
|
||||||
|
useInternalTracks: true,
|
||||||
|
internalNodeType: InternalNodeType.STATION,
|
||||||
|
trackDefinitions: [
|
||||||
|
{
|
||||||
|
startPoint: Direction.SOUTH,
|
||||||
|
type: TrackType.RAIL,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const TURN_RAIL_TO_ROAD: Piece = new Piece({
|
||||||
|
useInternalTracks: true,
|
||||||
|
internalNodeType: InternalNodeType.STATION,
|
||||||
|
trackDefinitions: [
|
||||||
|
{
|
||||||
|
startPoint: Direction.SOUTH,
|
||||||
|
type: TrackType.RAIL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.EAST,
|
||||||
|
type: TrackType.ROAD,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const T_JUNCTION_WITH_ROAD_ON_SIDE: Piece = new Piece({
|
||||||
|
useInternalTracks: true,
|
||||||
|
internalNodeType: InternalNodeType.STATION,
|
||||||
|
trackDefinitions: [
|
||||||
|
{
|
||||||
|
startPoint: Direction.SOUTH,
|
||||||
|
type: TrackType.RAIL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.WEST,
|
||||||
|
type: TrackType.RAIL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.EAST,
|
||||||
|
type: TrackType.ROAD,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const FOUR_WAY_WITH_ONE_ROAD: Piece = new Piece({
|
||||||
|
useInternalTracks: true,
|
||||||
|
internalNodeType: InternalNodeType.STATION,
|
||||||
|
trackDefinitions: [
|
||||||
|
{
|
||||||
|
startPoint: Direction.NORTH,
|
||||||
|
type: TrackType.RAIL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.EAST,
|
||||||
|
type: TrackType.ROAD,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.SOUTH,
|
||||||
|
type: TrackType.RAIL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.WEST,
|
||||||
|
type: TrackType.RAIL,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const T_JUNCTION_WITH_ROAD_AT_CENTER: Piece = new Piece({
|
||||||
|
useInternalTracks: true,
|
||||||
|
internalNodeType: InternalNodeType.STATION,
|
||||||
|
trackDefinitions: [
|
||||||
|
{
|
||||||
|
startPoint: Direction.NORTH,
|
||||||
|
type: TrackType.RAIL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.EAST,
|
||||||
|
type: TrackType.ROAD,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.SOUTH,
|
||||||
|
type: TrackType.RAIL,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const STRAIGHT_TRACK_CHANGE: Piece = new Piece({
|
||||||
|
useInternalTracks: true,
|
||||||
|
internalNodeType: InternalNodeType.STATION,
|
||||||
|
trackDefinitions: [
|
||||||
|
{
|
||||||
|
startPoint: Direction.EAST,
|
||||||
|
type: TrackType.ROAD,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.WEST,
|
||||||
|
type: TrackType.RAIL,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const DEAD_END_STATION_ROAD: Piece = new Piece({
|
||||||
|
useInternalTracks: true,
|
||||||
|
internalNodeType: InternalNodeType.STATION,
|
||||||
|
trackDefinitions: [
|
||||||
|
{
|
||||||
|
startPoint: Direction.EAST,
|
||||||
|
type: TrackType.ROAD,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const T_JUNCTION_WITH_RAIL_AT_CENTER: Piece = new Piece({
|
||||||
|
useInternalTracks: true,
|
||||||
|
internalNodeType: InternalNodeType.STATION,
|
||||||
|
trackDefinitions: [
|
||||||
|
{
|
||||||
|
startPoint: Direction.EAST,
|
||||||
|
type: TrackType.ROAD,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.SOUTH,
|
||||||
|
type: TrackType.RAIL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.WEST,
|
||||||
|
type: TrackType.ROAD,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const FOUR_WAY_PERPENDICULAR_CROSSING: Piece = new Piece({
|
||||||
|
useInternalTracks: true,
|
||||||
|
internalNodeType: InternalNodeType.STATION,
|
||||||
|
trackDefinitions: [
|
||||||
|
{
|
||||||
|
startPoint: Direction.NORTH,
|
||||||
|
type: TrackType.RAIL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.SOUTH,
|
||||||
|
type: TrackType.RAIL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.WEST,
|
||||||
|
type: TrackType.ROAD,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.EAST,
|
||||||
|
type: TrackType.ROAD,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const FOUR_WAY_WITH_ONE_RAIL: Piece = new Piece({
|
||||||
|
useInternalTracks: true,
|
||||||
|
internalNodeType: InternalNodeType.STATION,
|
||||||
|
trackDefinitions: [
|
||||||
|
{
|
||||||
|
startPoint: Direction.NORTH,
|
||||||
|
type: TrackType.ROAD,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.EAST,
|
||||||
|
type: TrackType.ROAD,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.SOUTH,
|
||||||
|
type: TrackType.RAIL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.WEST,
|
||||||
|
type: TrackType.ROAD,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const FOUR_WAY_TURNING_CROSSING: Piece = new Piece({
|
||||||
|
useInternalTracks: true,
|
||||||
|
internalNodeType: InternalNodeType.STATION,
|
||||||
|
trackDefinitions: [
|
||||||
|
{
|
||||||
|
startPoint: Direction.NORTH,
|
||||||
|
type: TrackType.ROAD,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.EAST,
|
||||||
|
type: TrackType.ROAD,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.SOUTH,
|
||||||
|
type: TrackType.RAIL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.WEST,
|
||||||
|
type: TrackType.RAIL,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const LEVEL_CROSSING: Piece = new Piece({
|
||||||
|
useInternalTracks: false,
|
||||||
|
trackDefinitions: [
|
||||||
|
{
|
||||||
|
startPoint: Direction.NORTH,
|
||||||
|
endPoint: Direction.SOUTH,
|
||||||
|
type: TrackType.RAIL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.EAST,
|
||||||
|
endPoint: Direction.WEST,
|
||||||
|
type: TrackType.ROAD,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const STRAIGHT_ROAD: Piece = new Piece({
|
||||||
|
useInternalTracks: false,
|
||||||
|
trackDefinitions: [
|
||||||
|
{
|
||||||
|
startPoint: Direction.EAST,
|
||||||
|
endPoint: Direction.WEST,
|
||||||
|
type: TrackType.ROAD,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const T_JUNCTION_ROAD: Piece = new Piece({
|
||||||
|
useInternalTracks: true,
|
||||||
|
internalNodeType: InternalNodeType.NONE,
|
||||||
|
trackDefinitions: [
|
||||||
|
{
|
||||||
|
startPoint: Direction.EAST,
|
||||||
|
type: TrackType.ROAD,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.SOUTH,
|
||||||
|
type: TrackType.ROAD,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.WEST,
|
||||||
|
type: TrackType.ROAD,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const FOUR_WAY_CROSS_ROAD: Piece = new Piece({
|
||||||
|
useInternalTracks: true,
|
||||||
|
internalNodeType: InternalNodeType.NONE,
|
||||||
|
trackDefinitions: [
|
||||||
|
{
|
||||||
|
startPoint: Direction.NORTH,
|
||||||
|
type: TrackType.ROAD,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.EAST,
|
||||||
|
type: TrackType.ROAD,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.SOUTH,
|
||||||
|
type: TrackType.ROAD,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.WEST,
|
||||||
|
type: TrackType.ROAD,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const DOUBLE_TURN_ROAD: Piece = new Piece({
|
||||||
|
useInternalTracks: false,
|
||||||
|
trackDefinitions: [
|
||||||
|
{
|
||||||
|
startPoint: Direction.NORTH,
|
||||||
|
endPoint: Direction.WEST,
|
||||||
|
type: TrackType.ROAD,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.EAST,
|
||||||
|
endPoint: Direction.SOUTH,
|
||||||
|
type: TrackType.ROAD,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const TURN_ROAD: Piece = new Piece({
|
||||||
|
useInternalTracks: false,
|
||||||
|
trackDefinitions: [
|
||||||
|
{
|
||||||
|
startPoint: Direction.NORTH,
|
||||||
|
endPoint: Direction.WEST,
|
||||||
|
type: TrackType.ROAD,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const DOUBLE_TURN_RAIL: Piece = new Piece({
|
||||||
|
useInternalTracks: false,
|
||||||
|
trackDefinitions: [
|
||||||
|
{
|
||||||
|
startPoint: Direction.NORTH,
|
||||||
|
endPoint: Direction.WEST,
|
||||||
|
type: TrackType.RAIL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: Direction.EAST,
|
||||||
|
endPoint: Direction.SOUTH,
|
||||||
|
type: TrackType.RAIL,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
export enum PieceId {
|
||||||
|
P01 = "P01",
|
||||||
|
P02 = "P02",
|
||||||
|
P03 = "P03",
|
||||||
|
P04 = "P04",
|
||||||
|
P05 = "P05",
|
||||||
|
P06 = "P06",
|
||||||
|
P07 = "P07",
|
||||||
|
P08 = "P08",
|
||||||
|
P09 = "P09",
|
||||||
|
P10 = "P10",
|
||||||
|
P11 = "P11",
|
||||||
|
P12 = "P12",
|
||||||
|
P13 = "P13",
|
||||||
|
P14 = "P14",
|
||||||
|
P15 = "P15",
|
||||||
|
P16 = "P16",
|
||||||
|
P17 = "P17",
|
||||||
|
P18 = "P18",
|
||||||
|
P19 = "P19",
|
||||||
|
P20 = "P20",
|
||||||
|
P21 = "P21",
|
||||||
|
P22 = "P22",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pieceMap: Record<PieceId, Piece> = {
|
||||||
|
[PieceId.P01]: STRAIGHT_RAIL,
|
||||||
|
[PieceId.P02]: TURN_RAIL,
|
||||||
|
[PieceId.P03]: FOUR_WAY_CROSS_RAIL,
|
||||||
|
[PieceId.P04]: T_JUNCTION_RAIL,
|
||||||
|
[PieceId.P05]: DEAD_END_STATION_RAIL,
|
||||||
|
[PieceId.P06]: TURN_RAIL_TO_ROAD,
|
||||||
|
[PieceId.P07]: T_JUNCTION_WITH_ROAD_ON_SIDE,
|
||||||
|
[PieceId.P08]: FOUR_WAY_WITH_ONE_ROAD,
|
||||||
|
[PieceId.P09]: T_JUNCTION_WITH_ROAD_AT_CENTER,
|
||||||
|
[PieceId.P10]: STRAIGHT_TRACK_CHANGE,
|
||||||
|
[PieceId.P11]: DEAD_END_STATION_ROAD,
|
||||||
|
[PieceId.P12]: T_JUNCTION_WITH_RAIL_AT_CENTER,
|
||||||
|
[PieceId.P13]: FOUR_WAY_PERPENDICULAR_CROSSING,
|
||||||
|
[PieceId.P14]: FOUR_WAY_WITH_ONE_RAIL,
|
||||||
|
[PieceId.P15]: FOUR_WAY_TURNING_CROSSING,
|
||||||
|
[PieceId.P16]: LEVEL_CROSSING,
|
||||||
|
[PieceId.P17]: STRAIGHT_ROAD,
|
||||||
|
[PieceId.P18]: T_JUNCTION_ROAD,
|
||||||
|
[PieceId.P19]: FOUR_WAY_CROSS_ROAD,
|
||||||
|
[PieceId.P20]: DOUBLE_TURN_ROAD,
|
||||||
|
[PieceId.P21]: TURN_ROAD,
|
||||||
|
[PieceId.P22]: DOUBLE_TURN_RAIL,
|
||||||
|
};
|
||||||
|
|
@ -1,7 +1,23 @@
|
||||||
|
export * from "./constants/CellType";
|
||||||
|
export * from "./constants/Direction";
|
||||||
|
export * from "./constants/ExitType";
|
||||||
|
export * from "./constants/InternalNodeType";
|
||||||
|
export * from "./constants/Pieces";
|
||||||
export * from "./constants/TrackType";
|
export * from "./constants/TrackType";
|
||||||
|
export * from "./server-events/CreateLobbyError";
|
||||||
|
export * from "./server-events/JoinLobbyError";
|
||||||
export * from "./server-events/ServerError";
|
export * from "./server-events/ServerError";
|
||||||
export * from "./server-events/ServerEvent";
|
export * from "./server-events/ServerEvent";
|
||||||
|
export * from "./server-events/StartRoundEvent";
|
||||||
export * from "./server-events/UpdateLobbyEvent";
|
export * from "./server-events/UpdateLobbyEvent";
|
||||||
export * from "./client-events/ClientEvent";
|
export * from "./client-events/ClientEvent";
|
||||||
export * from "./client-events/CreateLobbyEvent";
|
export * from "./client-events/CreateLobbyEvent";
|
||||||
export * from "./client-events/JoinLobbyEvent";
|
export * from "./client-events/JoinLobbyEvent";
|
||||||
|
export * from "./types/Border";
|
||||||
|
export * from "./types/Cell";
|
||||||
|
export * from "./types/Exit";
|
||||||
|
export * from "./types/ExternalNode";
|
||||||
|
export * from "./types/InternalNode";
|
||||||
|
export * from "./types/Piece";
|
||||||
|
export * from "./types/PlacedPiece";
|
||||||
|
export * from "./BoardBuilder";
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"socket.io-client": "^4.8.1"
|
"socket.io-client": "^4.8.1",
|
||||||
|
"uuid": "^11.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^8.0.0",
|
"@eslint/js": "^8.0.0",
|
||||||
|
|
@ -247,12 +248,6 @@
|
||||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/cookie": {
|
|
||||||
"version": "0.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
|
|
||||||
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/cors": {
|
"node_modules/@types/cors": {
|
||||||
"version": "2.8.17",
|
"version": "2.8.17",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
|
||||||
|
|
@ -271,21 +266,21 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.15.0",
|
"version": "8.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.23.0.tgz",
|
||||||
"integrity": "sha512-+zkm9AR1Ds9uLWN3fkoeXgFppaQ+uEVtfOV62dDmsy9QCNqlRHWNEck4yarvRNrvRcHQLGfqBNui3cimoz8XAg==",
|
"integrity": "sha512-vBz65tJgRrA1Q5gWlRfvoH+w943dq9K1p1yDBY2pc+a1nbBLZp7fB9+Hk8DaALUbzjqlMfgaqlVPT1REJdkt/w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.10.0",
|
"@eslint-community/regexpp": "^4.10.0",
|
||||||
"@typescript-eslint/scope-manager": "8.15.0",
|
"@typescript-eslint/scope-manager": "8.23.0",
|
||||||
"@typescript-eslint/type-utils": "8.15.0",
|
"@typescript-eslint/type-utils": "8.23.0",
|
||||||
"@typescript-eslint/utils": "8.15.0",
|
"@typescript-eslint/utils": "8.23.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.15.0",
|
"@typescript-eslint/visitor-keys": "8.23.0",
|
||||||
"graphemer": "^1.4.0",
|
"graphemer": "^1.4.0",
|
||||||
"ignore": "^5.3.1",
|
"ignore": "^5.3.1",
|
||||||
"natural-compare": "^1.4.0",
|
"natural-compare": "^1.4.0",
|
||||||
"ts-api-utils": "^1.3.0"
|
"ts-api-utils": "^2.0.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -296,25 +291,21 @@
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
|
"@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
|
||||||
"eslint": "^8.57.0 || ^9.0.0"
|
"eslint": "^8.57.0 || ^9.0.0",
|
||||||
},
|
"typescript": ">=4.8.4 <5.8.0"
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"typescript": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "8.15.0",
|
"version": "8.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.23.0.tgz",
|
||||||
"integrity": "sha512-7n59qFpghG4uazrF9qtGKBZXn7Oz4sOMm8dwNWDQY96Xlm2oX67eipqcblDj+oY1lLCbf1oltMZFpUso66Kl1A==",
|
"integrity": "sha512-h2lUByouOXFAlMec2mILeELUbME5SZRN/7R9Cw2RD2lRQQY08MWMM+PmVVKKJNK1aIwqTo9t/0CvOxwPbRIE2Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.15.0",
|
"@typescript-eslint/scope-manager": "8.23.0",
|
||||||
"@typescript-eslint/types": "8.15.0",
|
"@typescript-eslint/types": "8.23.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.15.0",
|
"@typescript-eslint/typescript-estree": "8.23.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.15.0",
|
"@typescript-eslint/visitor-keys": "8.23.0",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -325,23 +316,19 @@
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": "^8.57.0 || ^9.0.0"
|
"eslint": "^8.57.0 || ^9.0.0",
|
||||||
},
|
"typescript": ">=4.8.4 <5.8.0"
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"typescript": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/scope-manager": {
|
"node_modules/@typescript-eslint/scope-manager": {
|
||||||
"version": "8.15.0",
|
"version": "8.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.23.0.tgz",
|
||||||
"integrity": "sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA==",
|
"integrity": "sha512-OGqo7+dXHqI7Hfm+WqkZjKjsiRtFUQHPdGMXzk5mYXhJUedO7e/Y7i8AK3MyLMgZR93TX4bIzYrfyVjLC+0VSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.15.0",
|
"@typescript-eslint/types": "8.23.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.15.0"
|
"@typescript-eslint/visitor-keys": "8.23.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -352,16 +339,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/type-utils": {
|
"node_modules/@typescript-eslint/type-utils": {
|
||||||
"version": "8.15.0",
|
"version": "8.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.23.0.tgz",
|
||||||
"integrity": "sha512-UU6uwXDoI3JGSXmcdnP5d8Fffa2KayOhUUqr/AiBnG1Gl7+7ut/oyagVeSkh7bxQ0zSXV9ptRh/4N15nkCqnpw==",
|
"integrity": "sha512-iIuLdYpQWZKbiH+RkCGc6iu+VwscP5rCtQ1lyQ7TYuKLrcZoeJVpcLiG8DliXVkUxirW/PWlmS+d6yD51L9jvA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/typescript-estree": "8.15.0",
|
"@typescript-eslint/typescript-estree": "8.23.0",
|
||||||
"@typescript-eslint/utils": "8.15.0",
|
"@typescript-eslint/utils": "8.23.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"ts-api-utils": "^1.3.0"
|
"ts-api-utils": "^2.0.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -371,18 +358,14 @@
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": "^8.57.0 || ^9.0.0"
|
"eslint": "^8.57.0 || ^9.0.0",
|
||||||
},
|
"typescript": ">=4.8.4 <5.8.0"
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"typescript": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/types": {
|
"node_modules/@typescript-eslint/types": {
|
||||||
"version": "8.15.0",
|
"version": "8.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.23.0.tgz",
|
||||||
"integrity": "sha512-n3Gt8Y/KyJNe0S3yDCD2RVKrHBC4gTUcLTebVBXacPy091E6tNspFLKRXlk3hwT4G55nfr1n2AdFqi/XMxzmPQ==",
|
"integrity": "sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -394,45 +377,20 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree": {
|
"node_modules/@typescript-eslint/typescript-estree": {
|
||||||
"version": "8.15.0",
|
"version": "8.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.23.0.tgz",
|
||||||
"integrity": "sha512-1eMp2JgNec/niZsR7ioFBlsh/Fk0oJbhaqO0jRyQBMgkz7RrFfkqF9lYYmBoGBaSiLnu8TAPQTwoTUiSTUW9dg==",
|
"integrity": "sha512-LcqzfipsB8RTvH8FX24W4UUFk1bl+0yTOf9ZA08XngFwMg4Kj8A+9hwz8Cr/ZS4KwHrmo9PJiLZkOt49vPnuvQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.15.0",
|
"@typescript-eslint/types": "8.23.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.15.0",
|
"@typescript-eslint/visitor-keys": "8.23.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
"minimatch": "^9.0.4",
|
"minimatch": "^9.0.4",
|
||||||
"semver": "^7.6.0",
|
"semver": "^7.6.0",
|
||||||
"ts-api-utils": "^1.3.0"
|
"ts-api-utils": "^2.0.1"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"typescript": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@typescript-eslint/utils": {
|
|
||||||
"version": "8.15.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.15.0.tgz",
|
|
||||||
"integrity": "sha512-k82RI9yGhr0QM3Dnq+egEpz9qB6Un+WLYhmoNcvl8ltMEededhh7otBVVIDDsEEttauwdY/hQoSsOv13lxrFzQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@eslint-community/eslint-utils": "^4.4.0",
|
|
||||||
"@typescript-eslint/scope-manager": "8.15.0",
|
|
||||||
"@typescript-eslint/types": "8.15.0",
|
|
||||||
"@typescript-eslint/typescript-estree": "8.15.0"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -442,22 +400,41 @@
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": "^8.57.0 || ^9.0.0"
|
"typescript": ">=4.8.4 <5.8.0"
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"typescript": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/visitor-keys": {
|
"node_modules/@typescript-eslint/utils": {
|
||||||
"version": "8.15.0",
|
"version": "8.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.23.0.tgz",
|
||||||
"integrity": "sha512-h8vYOulWec9LhpwfAdZf2bjr8xIp0KNKnpgqSz0qqYYKAW/QZKw3ktRndbiAtUz4acH4QLQavwZBYCc0wulA/Q==",
|
"integrity": "sha512-uB/+PSo6Exu02b5ZEiVtmY6RVYO7YU5xqgzTIVZwTHvvK3HsL8tZZHFaTLFtRG3CsV4A5mhOv+NZx5BlhXPyIA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.15.0",
|
"@eslint-community/eslint-utils": "^4.4.0",
|
||||||
|
"@typescript-eslint/scope-manager": "8.23.0",
|
||||||
|
"@typescript-eslint/types": "8.23.0",
|
||||||
|
"@typescript-eslint/typescript-estree": "8.23.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"eslint": "^8.57.0 || ^9.0.0",
|
||||||
|
"typescript": ">=4.8.4 <5.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/visitor-keys": {
|
||||||
|
"version": "8.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.23.0.tgz",
|
||||||
|
"integrity": "sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@typescript-eslint/types": "8.23.0",
|
||||||
"eslint-visitor-keys": "^4.2.0"
|
"eslint-visitor-keys": "^4.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -742,12 +719,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/engine.io": {
|
"node_modules/engine.io": {
|
||||||
"version": "6.6.2",
|
"version": "6.6.4",
|
||||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
|
||||||
"integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==",
|
"integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/cookie": "^0.4.1",
|
|
||||||
"@types/cors": "^2.8.12",
|
"@types/cors": "^2.8.12",
|
||||||
"@types/node": ">=10.0.0",
|
"@types/node": ">=10.0.0",
|
||||||
"accepts": "~1.3.4",
|
"accepts": "~1.3.4",
|
||||||
|
|
@ -763,9 +739,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/engine.io-client": {
|
"node_modules/engine.io-client": {
|
||||||
"version": "6.6.2",
|
"version": "6.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
|
||||||
"integrity": "sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw==",
|
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@socket.io/component-emitter": "~3.1.0",
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
|
@ -1047,9 +1023,9 @@
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/fast-glob": {
|
"node_modules/fast-glob": {
|
||||||
"version": "3.3.2",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||||
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
|
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -1057,7 +1033,7 @@
|
||||||
"@nodelib/fs.walk": "^1.2.3",
|
"@nodelib/fs.walk": "^1.2.3",
|
||||||
"glob-parent": "^5.1.2",
|
"glob-parent": "^5.1.2",
|
||||||
"merge2": "^1.3.0",
|
"merge2": "^1.3.0",
|
||||||
"micromatch": "^4.0.4"
|
"micromatch": "^4.0.8"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.6.0"
|
"node": ">=8.6.0"
|
||||||
|
|
@ -1790,9 +1766,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.6.3",
|
"version": "7.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||||
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
|
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -1958,16 +1934,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ts-api-utils": {
|
"node_modules/ts-api-utils": {
|
||||||
"version": "1.4.0",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz",
|
||||||
"integrity": "sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==",
|
"integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">=18.12"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": ">=4.2.0"
|
"typescript": ">=4.8.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
|
|
@ -2017,15 +1993,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript-eslint": {
|
"node_modules/typescript-eslint": {
|
||||||
"version": "8.15.0",
|
"version": "8.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.23.0.tgz",
|
||||||
"integrity": "sha512-wY4FRGl0ZI+ZU4Jo/yjdBu0lVTSML58pu6PgGtJmCufvzfV565pUF6iACQt092uFOd49iLOTX/sEVmHtbSrS+w==",
|
"integrity": "sha512-/LBRo3HrXr5LxmrdYSOCvoAMm7p2jNizNfbIpCgvG4HMsnoprRUOce/+8VJ9BDYWW68rqIENE/haVLWPeFZBVQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "8.15.0",
|
"@typescript-eslint/eslint-plugin": "8.23.0",
|
||||||
"@typescript-eslint/parser": "8.15.0",
|
"@typescript-eslint/parser": "8.23.0",
|
||||||
"@typescript-eslint/utils": "8.15.0"
|
"@typescript-eslint/utils": "8.23.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -2035,12 +2011,8 @@
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": "^8.57.0 || ^9.0.0"
|
"eslint": "^8.57.0 || ^9.0.0",
|
||||||
},
|
"typescript": ">=4.8.4 <5.8.0"
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"typescript": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
|
|
@ -2058,6 +2030,19 @@
|
||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "11.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz",
|
||||||
|
"integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/esm/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vary": {
|
"node_modules/vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"socket.io-client": "^4.8.1"
|
"socket.io-client": "^4.8.1",
|
||||||
|
"uuid": "^11.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Socket as ServerSocket } from "socket.io";
|
||||||
|
import { Socket as ClientSocket } from "socket.io-client";
|
||||||
|
import { ErrorCode, ServerError } from "./ServerError";
|
||||||
|
|
||||||
|
export type CreateLobbyError = {
|
||||||
|
error: ErrorCode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const emitCreateLobbyError = (
|
||||||
|
socket: ServerSocket,
|
||||||
|
payload: CreateLobbyError,
|
||||||
|
) => {
|
||||||
|
socket.emit(ServerError.CREATE_LOBBY_ERROR, payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const attachHandlerToCreateLobbyError = (
|
||||||
|
socket: ClientSocket,
|
||||||
|
handler: (payload: CreateLobbyError) => void,
|
||||||
|
): (() => void) => {
|
||||||
|
socket.on(ServerError.CREATE_LOBBY_ERROR, handler);
|
||||||
|
return () => socket.off(ServerError.CREATE_LOBBY_ERROR);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Socket as ServerSocket } from "socket.io";
|
||||||
|
import { Socket as ClientSocket } from "socket.io-client";
|
||||||
|
import { ErrorCode, ServerError } from "./ServerError";
|
||||||
|
|
||||||
|
export type JoinLobbyError = {
|
||||||
|
error: ErrorCode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const emitJoinLobbyError = (
|
||||||
|
socket: ServerSocket,
|
||||||
|
payload: JoinLobbyError,
|
||||||
|
) => {
|
||||||
|
socket.emit(ServerError.JOIN_LOBBY_ERROR, payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const attachHandlerToJoinLobbyError = (
|
||||||
|
socket: ClientSocket,
|
||||||
|
handler: (payload: JoinLobbyError) => void,
|
||||||
|
): (() => void) => {
|
||||||
|
socket.on(ServerError.JOIN_LOBBY_ERROR, handler);
|
||||||
|
return () => socket.off(ServerError.JOIN_LOBBY_ERROR);
|
||||||
|
};
|
||||||
|
|
@ -2,3 +2,12 @@ export enum ServerError {
|
||||||
CREATE_LOBBY_ERROR = "create-lobby-error",
|
CREATE_LOBBY_ERROR = "create-lobby-error",
|
||||||
JOIN_LOBBY_ERROR = "join-lobby-error",
|
JOIN_LOBBY_ERROR = "join-lobby-error",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ErrorCode {
|
||||||
|
PLAYER_NOT_FOUND = "player-not-found",
|
||||||
|
GAME_NOT_FOUND = "game-not-found",
|
||||||
|
MISSING_USER_NAME = "missing-player-name",
|
||||||
|
INVALID_USER_NAME = "invalid-player-name",
|
||||||
|
DUPLICATE_USER_NAME = "duplicate-player-name",
|
||||||
|
MISSING_GAME_CODE = "missing-game-code",
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Socket as ServerSocket } from "socket.io";
|
||||||
|
import { Socket as ClientSocket } from "socket.io-client";
|
||||||
|
import { ServerEvent } from "./ServerEvent";
|
||||||
|
import { PieceId } from "../constants/Pieces";
|
||||||
|
|
||||||
|
export type StartRoundEvent = {
|
||||||
|
pieceIds: PieceId[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const emitStartRoundEvent = (
|
||||||
|
socket: ServerSocket,
|
||||||
|
payload: StartRoundEvent,
|
||||||
|
) => {
|
||||||
|
socket.emit(ServerEvent.START_ROUND, payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function attachHandlerToStartRoundEvent(
|
||||||
|
socket: ClientSocket,
|
||||||
|
handler: (event: StartRoundEvent) => void,
|
||||||
|
): void {
|
||||||
|
socket.once(ServerEvent.START_ROUND, handler);
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,22 @@
|
||||||
import { Socket } from "socket.io-client";
|
import { Socket as ServerSocket } from "socket.io";
|
||||||
|
import { Socket as ClientSocket } from "socket.io-client";
|
||||||
import { ServerEvent } from "./ServerEvent";
|
import { ServerEvent } from "./ServerEvent";
|
||||||
|
|
||||||
export type UpdateLobbyEvent = {
|
export type UpdateLobbyEvent = {
|
||||||
playerNames: Array<string>;
|
playerNames: Array<string>;
|
||||||
|
gameCode: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const emitUpdateLobbyEvent = (
|
||||||
|
socket: ServerSocket,
|
||||||
|
payload: UpdateLobbyEvent,
|
||||||
|
) => {
|
||||||
|
socket.emit(ServerEvent.LOBBY_UPDATE, payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function attachHandlerToUpdateLobbyEvent(
|
export function attachHandlerToUpdateLobbyEvent(
|
||||||
socket: Socket,
|
socket: ClientSocket,
|
||||||
handler: (event: UpdateLobbyEvent) => void,
|
handler: (event: UpdateLobbyEvent) => void,
|
||||||
): () => void {
|
): void {
|
||||||
socket.on(ServerEvent.LOBBY_UPDATE, handler);
|
socket.once(ServerEvent.LOBBY_UPDATE, handler);
|
||||||
return () => socket.off(ServerEvent.LOBBY_UPDATE, handler);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Exit } from "./Exit";
|
||||||
|
import { ExternalNode } from "./ExternalNode";
|
||||||
|
|
||||||
|
export class Border {
|
||||||
|
private readonly firstNode: ExternalNode;
|
||||||
|
private readonly secondNode: ExternalNode | Exit;
|
||||||
|
|
||||||
|
constructor(firstNode: ExternalNode, secondNode: ExternalNode | Exit) {
|
||||||
|
this.firstNode = firstNode;
|
||||||
|
this.secondNode = secondNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public traverseFrom(node: ExternalNode): ExternalNode | Exit {
|
||||||
|
if (node === this.firstNode) {
|
||||||
|
return this.secondNode;
|
||||||
|
}
|
||||||
|
if (node === this.secondNode) {
|
||||||
|
return this.firstNode;
|
||||||
|
}
|
||||||
|
throw Error("Unable to traverse border");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
import { CellType } from "../constants/CellType";
|
||||||
|
import { Direction } from "../constants/Direction";
|
||||||
|
import { ExitType } from "../constants/ExitType";
|
||||||
|
import { PieceId, pieceMap } from "../constants/Pieces";
|
||||||
|
import { Exit } from "./Exit";
|
||||||
|
import { ExternalNode } from "./ExternalNode";
|
||||||
|
import { InternalNode } from "./InternalNode";
|
||||||
|
import { Piece } from "./Piece";
|
||||||
|
import { PlacedPiece } from "./PlacedPiece";
|
||||||
|
|
||||||
|
export class Cell {
|
||||||
|
public readonly externalNodes: Map<Direction, ExternalNode>;
|
||||||
|
public readonly cellType: CellType;
|
||||||
|
public placedPiece?: {
|
||||||
|
piece: PlacedPiece;
|
||||||
|
id: PieceId;
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(cellType: CellType) {
|
||||||
|
this.externalNodes = new Map([
|
||||||
|
[Direction.NORTH, new ExternalNode(this, Direction.NORTH)],
|
||||||
|
[Direction.SOUTH, new ExternalNode(this, Direction.SOUTH)],
|
||||||
|
[Direction.EAST, new ExternalNode(this, Direction.EAST)],
|
||||||
|
[Direction.WEST, new ExternalNode(this, Direction.WEST)],
|
||||||
|
]);
|
||||||
|
this.cellType = cellType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getNodeAt(direction: Direction): ExternalNode {
|
||||||
|
const node = this.externalNodes.get(direction);
|
||||||
|
if (!node) throw Error(`Could not find node at ${direction}`);
|
||||||
|
return node!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param rotation in degrees counter-clockwise
|
||||||
|
*/
|
||||||
|
public placePiece(pieceId: PieceId, rotation: 0 | 90 | 180 | 270) {
|
||||||
|
if (this.placedPiece !== undefined) return;
|
||||||
|
const piece: Piece = pieceMap[pieceId].rotate(rotation);
|
||||||
|
|
||||||
|
this.validatePiecePlacement(piece);
|
||||||
|
|
||||||
|
this.placedPiece = {
|
||||||
|
piece: piece.toPlacedPiece(this),
|
||||||
|
id: pieceId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private validatePiecePlacement(piece: Piece) {
|
||||||
|
const hasAnyConnection = Array.from(piece.tracks)
|
||||||
|
.map((track) => {
|
||||||
|
const trackExternalNodes = [
|
||||||
|
this.getNodeAt(track.joinedPoints.firstPoint),
|
||||||
|
];
|
||||||
|
if (!(track.joinedPoints.secondPoint instanceof InternalNode)) {
|
||||||
|
trackExternalNodes.push(
|
||||||
|
this.getNodeAt(track.joinedPoints.secondPoint),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
trackExternalNodes: trackExternalNodes,
|
||||||
|
trackType: track.type,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.reduce((isConnected, { trackExternalNodes, trackType }) => {
|
||||||
|
let isTrackConnected: boolean = false;
|
||||||
|
trackExternalNodes
|
||||||
|
.filter((node) => node.traverseBorder() instanceof Exit)
|
||||||
|
.forEach((node) => {
|
||||||
|
const exitType = (node.traverseBorder() as Exit).type;
|
||||||
|
isTrackConnected = true;
|
||||||
|
if (
|
||||||
|
exitType !== ExitType.AMBIVALENT &&
|
||||||
|
exitType.toString() !== trackType.toString()
|
||||||
|
) {
|
||||||
|
throw Error(
|
||||||
|
`Unable to place piece, invalid exit type at direction ${node.direction}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
trackExternalNodes
|
||||||
|
.filter((node) => node.traverseBorder() instanceof ExternalNode)
|
||||||
|
.forEach((node) => {
|
||||||
|
const adjacentExternalNode = node.traverseBorder() as ExternalNode;
|
||||||
|
const adjacentTrack =
|
||||||
|
adjacentExternalNode.cell.placedPiece?.piece.findTrackForDirection(
|
||||||
|
adjacentExternalNode.direction,
|
||||||
|
);
|
||||||
|
if (adjacentTrack !== undefined) {
|
||||||
|
isTrackConnected = true;
|
||||||
|
if (adjacentTrack.type !== trackType) {
|
||||||
|
throw Error(
|
||||||
|
`Unable to place piece next to another due to conflicting track types at ${node.direction}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return isConnected || isTrackConnected;
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
if (!hasAnyConnection) {
|
||||||
|
throw Error("No adjacent exit or piece available to connect to");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public removePiece() {
|
||||||
|
this.placedPiece = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { ExitType } from "../constants/ExitType";
|
||||||
|
|
||||||
|
export class Exit {
|
||||||
|
public readonly type: ExitType;
|
||||||
|
|
||||||
|
constructor(type: ExitType) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { Direction } from "../constants/Direction";
|
||||||
|
import { Border } from "./Border";
|
||||||
|
import { Cell } from "./Cell";
|
||||||
|
import { Exit } from "./Exit";
|
||||||
|
|
||||||
|
export class ExternalNode {
|
||||||
|
public readonly direction: Direction;
|
||||||
|
private border?: Border;
|
||||||
|
public readonly cell: Cell;
|
||||||
|
|
||||||
|
constructor(cell: Cell, direction: Direction) {
|
||||||
|
this.cell = cell;
|
||||||
|
this.direction = direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
public linkToNode(other: ExternalNode | Exit) {
|
||||||
|
this.border = new Border(this, other);
|
||||||
|
if (other instanceof ExternalNode) {
|
||||||
|
other.border = this.border;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public traverseBorder(): ExternalNode | Exit {
|
||||||
|
if (!this.border) {
|
||||||
|
throw Error(`Missing border for node`);
|
||||||
|
}
|
||||||
|
return this.border.traverseFrom(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { InternalNodeType } from "../constants/InternalNodeType";
|
||||||
|
|
||||||
|
export class InternalNode {
|
||||||
|
id: string;
|
||||||
|
type: InternalNodeType;
|
||||||
|
|
||||||
|
constructor(type: InternalNodeType) {
|
||||||
|
this.id = uuidv4();
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
import { Direction, rotateDirection } from "../constants/Direction";
|
||||||
|
import { TrackType } from "../constants/TrackType";
|
||||||
|
import { Cell } from "./Cell";
|
||||||
|
import { InternalNode } from "./InternalNode";
|
||||||
|
import { PlacedPiece } from "./PlacedPiece";
|
||||||
|
import { InternalNodeType } from "../constants/InternalNodeType";
|
||||||
|
|
||||||
|
export interface PieceProps {
|
||||||
|
readonly useInternalTracks: boolean;
|
||||||
|
readonly internalNodeType?: InternalNodeType;
|
||||||
|
readonly trackDefinitions: {
|
||||||
|
readonly startPoint: Direction;
|
||||||
|
readonly endPoint?: Direction;
|
||||||
|
readonly type: TrackType;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type Track = {
|
||||||
|
readonly joinedPoints: {
|
||||||
|
readonly firstPoint: Direction;
|
||||||
|
readonly secondPoint: Direction | InternalNode;
|
||||||
|
};
|
||||||
|
readonly type: TrackType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Piece {
|
||||||
|
readonly tracks: Set<Track>;
|
||||||
|
readonly internalNode?: InternalNode;
|
||||||
|
|
||||||
|
constructor(pieceProps: PieceProps) {
|
||||||
|
if (pieceProps.useInternalTracks) {
|
||||||
|
if (!pieceProps.internalNodeType) {
|
||||||
|
throw Error(
|
||||||
|
"Expected to find internal node type when useInternalTracks is set",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.internalNode = new InternalNode(pieceProps.internalNodeType);
|
||||||
|
this.tracks = new Set(
|
||||||
|
pieceProps.trackDefinitions.map((trackDefinition) => {
|
||||||
|
return {
|
||||||
|
joinedPoints: {
|
||||||
|
firstPoint: trackDefinition.startPoint,
|
||||||
|
secondPoint: this.internalNode!,
|
||||||
|
},
|
||||||
|
type: trackDefinition.type,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.internalNode = undefined;
|
||||||
|
this.tracks = new Set(
|
||||||
|
pieceProps.trackDefinitions.map((trackDefinition) => {
|
||||||
|
if (!trackDefinition.endPoint) {
|
||||||
|
throw Error("Missing end point for non-internal track");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
joinedPoints: {
|
||||||
|
firstPoint: trackDefinition.startPoint,
|
||||||
|
secondPoint: trackDefinition.endPoint,
|
||||||
|
},
|
||||||
|
type: trackDefinition.type,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rotate(rotation: 0 | 90 | 180 | 270): Piece {
|
||||||
|
return new Piece({
|
||||||
|
useInternalTracks: this.internalNode !== undefined,
|
||||||
|
internalNodeType: this.internalNode?.type,
|
||||||
|
trackDefinitions: Array.from(this.tracks).map((track) => {
|
||||||
|
if (track.joinedPoints.secondPoint instanceof InternalNode) {
|
||||||
|
return {
|
||||||
|
type: track.type,
|
||||||
|
startPoint: rotateDirection(
|
||||||
|
track.joinedPoints.firstPoint,
|
||||||
|
rotation,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
type: track.type,
|
||||||
|
startPoint: rotateDirection(
|
||||||
|
track.joinedPoints.firstPoint,
|
||||||
|
rotation,
|
||||||
|
),
|
||||||
|
endPoint: rotateDirection(track.joinedPoints.secondPoint, rotation),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toPlacedPiece(cell: Cell) {
|
||||||
|
return new PlacedPiece(
|
||||||
|
new Set(
|
||||||
|
Array.from(this.tracks).map((track) => {
|
||||||
|
return {
|
||||||
|
nodes: {
|
||||||
|
firstNode: cell.getNodeAt(track.joinedPoints.firstPoint),
|
||||||
|
secondNode:
|
||||||
|
track.joinedPoints.secondPoint instanceof InternalNode
|
||||||
|
? (track.joinedPoints.secondPoint as InternalNode)
|
||||||
|
: cell.getNodeAt(track.joinedPoints.secondPoint as Direction),
|
||||||
|
},
|
||||||
|
type: track.type,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
this.internalNode,
|
||||||
|
cell,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { Direction } from "../constants/Direction";
|
||||||
|
import { TrackType } from "../constants/TrackType";
|
||||||
|
import { Cell } from "./Cell";
|
||||||
|
import { ExternalNode } from "./ExternalNode";
|
||||||
|
import { InternalNode } from "./InternalNode";
|
||||||
|
|
||||||
|
export class PlacedPiece {
|
||||||
|
tracks: Set<{
|
||||||
|
nodes: {
|
||||||
|
firstNode: ExternalNode;
|
||||||
|
secondNode: ExternalNode | InternalNode;
|
||||||
|
};
|
||||||
|
type: TrackType;
|
||||||
|
}>;
|
||||||
|
internalNode?: InternalNode;
|
||||||
|
cell: Cell;
|
||||||
|
constructor(
|
||||||
|
tracks: Set<{
|
||||||
|
nodes: {
|
||||||
|
firstNode: ExternalNode;
|
||||||
|
secondNode: ExternalNode | InternalNode;
|
||||||
|
};
|
||||||
|
type: TrackType;
|
||||||
|
}>,
|
||||||
|
internalNodes: InternalNode | undefined,
|
||||||
|
cell: Cell,
|
||||||
|
) {
|
||||||
|
this.tracks = tracks;
|
||||||
|
this.internalNode = internalNodes;
|
||||||
|
this.cell = cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
public findTrackForDirection(direction: Direction) {
|
||||||
|
return Array.from(this.tracks).find(
|
||||||
|
(track) =>
|
||||||
|
track.nodes.firstNode.direction === direction ||
|
||||||
|
(track.nodes.secondNode instanceof ExternalNode &&
|
||||||
|
track.nodes.secondNode.direction === direction),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"@types/jest": "^27.5.2",
|
"@types/jest": "^27.5.2",
|
||||||
"@types/node": "^16.18.119",
|
"@types/node": "^17.0.29",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"interface": "file:../interface",
|
"interface": "file:../interface",
|
||||||
|
|
@ -4201,9 +4201,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "16.18.119",
|
"version": "17.0.29",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.119.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.29.tgz",
|
||||||
"integrity": "sha512-ia7V9a2FnhUFfetng4/sRPBMTwHZUkPFY736rb1cg9AgG7MZdR97q7/nLR9om+sq5f1la9C857E0l/nrI0RiFQ==",
|
"integrity": "sha512-tx5jMmMFwx7wBwq/V7OohKDVb/JwJU5qCVkeLMh1//xycAJ/ESuw9aJ9SEtlCZDYi2pBfe4JkisSoAtbOsBNAA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node-forge": {
|
"node_modules/@types/node-forge": {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"@types/jest": "^27.5.2",
|
"@types/jest": "^27.5.2",
|
||||||
"@types/node": "^16.18.119",
|
"@types/node": "^17.0.29",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"interface": "file:../interface",
|
"interface": "file:../interface",
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
|
@ -1,3 +1,9 @@
|
||||||
@use "node_modules/@picocss/pico/scss/pico" with (
|
@use "node_modules/@picocss/pico/scss/pico" with (
|
||||||
$theme-color: "pumpkin"
|
$theme-color: "pumpkin"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
#root, #app {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,56 @@
|
||||||
import React from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
import { Socket } from "socket.io-client";
|
import { Socket } from "socket.io-client";
|
||||||
import LandingPage from "./pages/landing/LandingPage";
|
import LandingPage from "./pages/landing/LandingPage";
|
||||||
|
import GamePage from "./pages/game/GamePage";
|
||||||
|
import "./App.scss";
|
||||||
|
import LobbyPage from "./pages/lobby/LobbyPage";
|
||||||
|
import {
|
||||||
|
attachHandlerToStartRoundEvent,
|
||||||
|
attachHandlerToUpdateLobbyEvent,
|
||||||
|
PieceId,
|
||||||
|
} from "interface";
|
||||||
|
|
||||||
export interface AppProps {
|
export interface AppProps {
|
||||||
socket: Socket;
|
socket: Socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
const App = (props: AppProps) => {
|
const App = (props: AppProps) => {
|
||||||
|
const { socket } = props;
|
||||||
|
const [renderedPage, setRenderedPage] = useState("LANDING");
|
||||||
|
|
||||||
|
const [initialPlayerNames, setInitialPlayerNames] = useState([] as string[]);
|
||||||
|
const [gameCode, setGameCode] = useState("");
|
||||||
|
const [initialPieceIds, setInitialPieceIds] = useState([] as PieceId[]);
|
||||||
|
|
||||||
|
const setupNextPageTransition = useCallback(() => {
|
||||||
|
if (renderedPage === "LANDING") {
|
||||||
|
attachHandlerToUpdateLobbyEvent(socket, (event) => {
|
||||||
|
setInitialPlayerNames(event.playerNames);
|
||||||
|
setGameCode(event.gameCode);
|
||||||
|
setRenderedPage("LOBBY");
|
||||||
|
});
|
||||||
|
} else if (renderedPage === "LOBBY") {
|
||||||
|
attachHandlerToStartRoundEvent(socket, (event) => {
|
||||||
|
setInitialPieceIds(event.pieceIds);
|
||||||
|
setRenderedPage("GAME");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [renderedPage]);
|
||||||
|
|
||||||
|
setupNextPageTransition();
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<LandingPage {...props} />
|
{renderedPage === "LANDING" && <LandingPage {...props} />}
|
||||||
|
{renderedPage === "LOBBY" && (
|
||||||
|
<LobbyPage
|
||||||
|
{...props}
|
||||||
|
initialPlayerNames={initialPlayerNames}
|
||||||
|
gameCode={gameCode}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{renderedPage === "GAME" && (
|
||||||
|
<GamePage initialPieceIds={initialPieceIds} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ const root = ReactDOM.createRoot(
|
||||||
document.getElementById("root") as HTMLElement,
|
document.getElementById("root") as HTMLElement,
|
||||||
);
|
);
|
||||||
const socket = io("http://localhost:3010");
|
const socket = io("http://localhost:3010");
|
||||||
|
socket.on("exception", console.log);
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App socket={socket} />
|
<App socket={socket} />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-panel {
|
||||||
|
user-select: none; /* Standard syntax */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
.right-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import GameBoard from "./components/GameBoard";
|
||||||
|
import DiceSet from "./components/DiceSet";
|
||||||
|
import "./GamePage.scss";
|
||||||
|
import { buildBoard, PieceId } from "interface";
|
||||||
|
import { DieViewProps } from "./types/DieViewProps";
|
||||||
|
import { PlacePieceActionStack } from "./types/PlacePieceActionStack";
|
||||||
|
|
||||||
|
export interface GamePageProps {
|
||||||
|
initialPieceIds: PieceId[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const GamePage = (props: GamePageProps) => {
|
||||||
|
const { initialPieceIds } = props;
|
||||||
|
|
||||||
|
const [pieceIds] = useState(initialPieceIds);
|
||||||
|
|
||||||
|
const getDiceSet: () => DieViewProps[] = () =>
|
||||||
|
pieceIds.map((pieceId) => {
|
||||||
|
return {
|
||||||
|
pieceId: pieceId,
|
||||||
|
isSelected: false,
|
||||||
|
isDisabled: false,
|
||||||
|
isSpecial: false,
|
||||||
|
rotation: 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const [dice, setDice] = useState(getDiceSet());
|
||||||
|
const [specialDice, setSpecialDice] = useState(
|
||||||
|
[
|
||||||
|
PieceId.P03,
|
||||||
|
PieceId.P08,
|
||||||
|
PieceId.P13,
|
||||||
|
PieceId.P14,
|
||||||
|
PieceId.P15,
|
||||||
|
PieceId.P19,
|
||||||
|
].map((pieceId) => {
|
||||||
|
return {
|
||||||
|
pieceId: pieceId,
|
||||||
|
isSelected: false,
|
||||||
|
isDisabled: false,
|
||||||
|
isSpecial: true,
|
||||||
|
rotation: 0,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const [specialDieUsedInRound, setSpecialDieUsedInRound] = useState(false);
|
||||||
|
|
||||||
|
const modifyDieState = (
|
||||||
|
matcher: (die: DieViewProps) => boolean,
|
||||||
|
newStateComputer: (die: DieViewProps) => Partial<DieViewProps>,
|
||||||
|
) => {
|
||||||
|
setDice(
|
||||||
|
dice.map((die) => {
|
||||||
|
if (!matcher(die)) return die;
|
||||||
|
return {
|
||||||
|
...die,
|
||||||
|
...newStateComputer(die),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
setSpecialDice(
|
||||||
|
specialDice.map((die) => {
|
||||||
|
if (!matcher(die)) return die;
|
||||||
|
return {
|
||||||
|
...die,
|
||||||
|
...newStateComputer(die),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchDie = (matcher: (die: DieViewProps) => boolean) => {
|
||||||
|
return dice.concat(specialDice).find((die) => matcher(die));
|
||||||
|
};
|
||||||
|
|
||||||
|
const [board, setBoard] = useState(buildBoard());
|
||||||
|
const [id, setId] = useState(1);
|
||||||
|
const refreshBoardRender = () => {
|
||||||
|
setBoard(board);
|
||||||
|
setId(id + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [placePieceActionStack, setPlacePieceActionStack] = useState(
|
||||||
|
new PlacePieceActionStack(),
|
||||||
|
);
|
||||||
|
const resetBoard = () => {
|
||||||
|
placePieceActionStack.resetActions(board);
|
||||||
|
setBoard(board);
|
||||||
|
modifyDieState(
|
||||||
|
() => true,
|
||||||
|
() => {
|
||||||
|
return { isSelected: false, isDisabled: false };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
setId(id + 1);
|
||||||
|
};
|
||||||
|
const commitBoard = () => {
|
||||||
|
if (dice.some((die) => !die.isDisabled)) return;
|
||||||
|
placePieceActionStack.commitActions();
|
||||||
|
setDice(getDiceSet());
|
||||||
|
setSpecialDieUsedInRound(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<h1>Game Page Title</h1>
|
||||||
|
<div className="game-panel" id={id.toString()}>
|
||||||
|
<GameBoard
|
||||||
|
modifyDieState={modifyDieState}
|
||||||
|
fetchDie={fetchDie}
|
||||||
|
setSpecialDieUsedInRound={setSpecialDieUsedInRound}
|
||||||
|
refreshBoardRender={refreshBoardRender}
|
||||||
|
board={board}
|
||||||
|
placePieceActionStack={placePieceActionStack}
|
||||||
|
setPlacePieceActionStack={setPlacePieceActionStack}
|
||||||
|
/>
|
||||||
|
<div className="right-panel">
|
||||||
|
<DiceSet
|
||||||
|
dice={dice}
|
||||||
|
specialDice={specialDice}
|
||||||
|
modifyDieState={modifyDieState}
|
||||||
|
specialDieUsedInRound={specialDieUsedInRound}
|
||||||
|
resetBoard={resetBoard}
|
||||||
|
commitBoard={commitBoard}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GamePage;
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
.cell {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border: 2px solid black;
|
||||||
|
border-radius: 10%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.piece {
|
||||||
|
border-radius: 10%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
|
&.rotate-90 {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rotate-180 {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rotate-270 {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-type-icon {
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
padding: 2px;
|
||||||
|
margin: 2px;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell .exit {
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
|
||||||
|
&:has(.exit-road) {
|
||||||
|
border: solid black;
|
||||||
|
border-width: 0 1px;
|
||||||
|
|
||||||
|
.exit-road {
|
||||||
|
margin: auto;
|
||||||
|
width: 0;
|
||||||
|
height: 100%;
|
||||||
|
border: dashed black;
|
||||||
|
border-width: 0 1px;
|
||||||
|
transform: translate(calc(50% - 1px));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(.exit-rail) {
|
||||||
|
border-width: 0 1px;
|
||||||
|
|
||||||
|
.exit-rail {
|
||||||
|
margin: auto;
|
||||||
|
width: 0;
|
||||||
|
height: 100%;
|
||||||
|
border: solid black;
|
||||||
|
border-width: 0 1px;
|
||||||
|
transform: translate(calc(50% - 1px));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(.exit-ambivalent) {
|
||||||
|
border-width: 0 1px;
|
||||||
|
border-style: dotted;
|
||||||
|
|
||||||
|
.exit-ambivalent {
|
||||||
|
margin: auto;
|
||||||
|
width: 0;
|
||||||
|
height: 100%;
|
||||||
|
border: dotted black;
|
||||||
|
border-width: 0 1px 0 0;
|
||||||
|
transform: translate(calc(50%));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:has(.exit-north), &:has(.exit-south) {
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(.exit-north) {
|
||||||
|
transform: translate(calc(-50% - 1px), -100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(.exit-south) {
|
||||||
|
top: 100%;
|
||||||
|
transform: translate(calc(-50% - 1px), 0%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(.exit-east), &:has(.exit-west) {
|
||||||
|
top: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(.exit-east) {
|
||||||
|
left: 100%;
|
||||||
|
transform: rotate(90deg) translate(-50%, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(.exit-west) {
|
||||||
|
left: 0%;
|
||||||
|
transform: rotate(90deg) translate(-50%, 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { Cell, CellType, directions, Exit, PieceId } from "interface";
|
||||||
|
import "./BoardCell.scss";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { DieViewProps } from "../types/DieViewProps";
|
||||||
|
|
||||||
|
export interface BoardCellProps {
|
||||||
|
cell: Cell;
|
||||||
|
refreshBoardRender: () => void;
|
||||||
|
modifyDieState: (
|
||||||
|
matcher: (die: DieViewProps) => boolean,
|
||||||
|
newStateComputer: (die: DieViewProps) => Partial<DieViewProps>,
|
||||||
|
) => void;
|
||||||
|
fetchDie: (
|
||||||
|
matcher: (die: DieViewProps) => boolean,
|
||||||
|
) => DieViewProps | undefined;
|
||||||
|
setSpecialDieUsedInRound: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
placePieceActionHandler: (pieceId: PieceId, rotation: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BoardCell = (props: BoardCellProps) => {
|
||||||
|
const {
|
||||||
|
cell,
|
||||||
|
refreshBoardRender,
|
||||||
|
modifyDieState,
|
||||||
|
fetchDie,
|
||||||
|
setSpecialDieUsedInRound,
|
||||||
|
placePieceActionHandler,
|
||||||
|
} = props;
|
||||||
|
const [pieceRotationAngle, setPieceRotationAngle] = useState(0);
|
||||||
|
const handleBoardCellClick = () => {
|
||||||
|
const selectedDie = fetchDie((die) => die.isSelected);
|
||||||
|
if (!selectedDie) return;
|
||||||
|
try {
|
||||||
|
placePieceActionHandler(selectedDie.pieceId, selectedDie.rotation);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modifyDieState(
|
||||||
|
(die) => die === selectedDie,
|
||||||
|
() => {
|
||||||
|
return {
|
||||||
|
isSelected: false,
|
||||||
|
isDisabled: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (selectedDie.isSpecial) setSpecialDieUsedInRound(true);
|
||||||
|
// Set rotation to the piece in the board, not the die
|
||||||
|
setPieceRotationAngle(selectedDie.rotation);
|
||||||
|
refreshBoardRender();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={"cell"} onClick={handleBoardCellClick}>
|
||||||
|
{directions.map((direction) => {
|
||||||
|
const traversedNode = cell.getNodeAt(direction).traverseBorder();
|
||||||
|
const isExit = traversedNode instanceof Exit;
|
||||||
|
if (!isExit) return;
|
||||||
|
const className =
|
||||||
|
`exit-${direction.toLowerCase()}` +
|
||||||
|
` exit-${(traversedNode as Exit).type.toLowerCase()}`;
|
||||||
|
return (
|
||||||
|
<div className="exit">
|
||||||
|
<div className={className} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{cell.placedPiece && (
|
||||||
|
<img
|
||||||
|
className={`piece rotate-${pieceRotationAngle}`}
|
||||||
|
src={`pieces/${cell.placedPiece.id}.jpeg`}
|
||||||
|
></img>
|
||||||
|
)}
|
||||||
|
{cell.cellType !== CellType.NORMAL && (
|
||||||
|
<img
|
||||||
|
className="cell-type-icon"
|
||||||
|
src={`./${cell.cellType.toLowerCase()}-icon.png`}
|
||||||
|
></img>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BoardCell;
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
.dice-set-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: max-content;
|
||||||
|
margin: auto auto 50px auto;
|
||||||
|
|
||||||
|
img {
|
||||||
|
border: 3px solid green;
|
||||||
|
box-shadow: 0 0 10px green;
|
||||||
|
border-radius: 20%;
|
||||||
|
padding: 5px;
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
margin-right: 10px;
|
||||||
|
|
||||||
|
&.icon-inverted {
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dice-set {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, auto);
|
||||||
|
gap: 20px;
|
||||||
|
margin: 0 0 50px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.special-dice-set {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, auto);
|
||||||
|
margin: 0 0 50px 0;
|
||||||
|
column-gap: 12%;
|
||||||
|
row-gap: 20px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
import "./DiceSet.scss";
|
||||||
|
import Die from "./Die";
|
||||||
|
import React from "react";
|
||||||
|
import { DieViewProps } from "../types/DieViewProps";
|
||||||
|
|
||||||
|
export interface DiceSetProps {
|
||||||
|
dice: DieViewProps[];
|
||||||
|
specialDice: DieViewProps[];
|
||||||
|
modifyDieState: (
|
||||||
|
matcher: (die: DieViewProps) => boolean,
|
||||||
|
newStateComputer: (die: DieViewProps) => Partial<DieViewProps>,
|
||||||
|
) => void;
|
||||||
|
specialDieUsedInRound: boolean;
|
||||||
|
resetBoard: () => void;
|
||||||
|
commitBoard: () => void;
|
||||||
|
}
|
||||||
|
const DiceSet = (props: DiceSetProps) => {
|
||||||
|
const {
|
||||||
|
dice,
|
||||||
|
specialDice,
|
||||||
|
modifyDieState,
|
||||||
|
specialDieUsedInRound,
|
||||||
|
resetBoard,
|
||||||
|
commitBoard,
|
||||||
|
} = props;
|
||||||
|
const handleDieClick = (clickedDie: DieViewProps) => {
|
||||||
|
if (clickedDie.isDisabled) return;
|
||||||
|
const isSpecialDie = clickedDie.isSpecial;
|
||||||
|
if (isSpecialDie && specialDieUsedInRound) return;
|
||||||
|
modifyDieState(
|
||||||
|
() => true,
|
||||||
|
(die) => {
|
||||||
|
return {
|
||||||
|
isSelected: die === clickedDie,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRotateButton = (rotation: number) => {
|
||||||
|
modifyDieState(
|
||||||
|
(die) => die.isSelected,
|
||||||
|
(die) => {
|
||||||
|
return {
|
||||||
|
rotation: (die.rotation + rotation + 360) % 360,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className="dice-set-actions">
|
||||||
|
<img
|
||||||
|
src="./rotate-icon.png"
|
||||||
|
onClick={() => handleRotateButton(-90)}
|
||||||
|
></img>
|
||||||
|
<img
|
||||||
|
src="./rotate-icon.png"
|
||||||
|
className="icon-inverted"
|
||||||
|
onClick={() => handleRotateButton(90)}
|
||||||
|
></img>
|
||||||
|
<img src="./clean-icon.png" onClick={resetBoard}></img>
|
||||||
|
<img src="./commit-icon.png" onClick={commitBoard}></img>
|
||||||
|
</div>
|
||||||
|
<div className="dice-set">
|
||||||
|
{dice.map((die) => (
|
||||||
|
<Die die={die} handleDieClick={handleDieClick} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="special-dice-set">
|
||||||
|
{specialDice.map((die) => (
|
||||||
|
<Die die={die} handleDieClick={handleDieClick} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DiceSet;
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
.dice {
|
||||||
|
height: 150px;
|
||||||
|
width: 150px;
|
||||||
|
border: 2px solid black;
|
||||||
|
border-radius: 10%;
|
||||||
|
|
||||||
|
img{
|
||||||
|
border-radius: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
box-shadow: 0 0 20px green;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
box-shadow: 0 0 20px grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rotate-90 {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rotate-180 {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rotate-270 {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
transition: transform 0.5s cubic-bezier(.47,1.64,.41,.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.special-dice-set .dice {
|
||||||
|
height: 80px;
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import "./Die.scss";
|
||||||
|
import { DieViewProps } from "../types/DieViewProps";
|
||||||
|
|
||||||
|
interface DieProps {
|
||||||
|
die: DieViewProps;
|
||||||
|
handleDieClick: (die: DieViewProps) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Die = (props: DieProps) => {
|
||||||
|
const { die, handleDieClick } = props;
|
||||||
|
const { pieceId, isSelected, isDisabled, rotation } = die;
|
||||||
|
const className =
|
||||||
|
"dice" +
|
||||||
|
(isSelected ? " selected" : "") +
|
||||||
|
(isDisabled ? " disabled" : "") +
|
||||||
|
` rotate-${rotation}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className} onClick={() => handleDieClick(die)}>
|
||||||
|
<img src={`pieces/${pieceId}.jpeg`}></img>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Die;
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
.game-board {
|
||||||
|
height: 850px;
|
||||||
|
width: 850px;
|
||||||
|
min-height: 850px;
|
||||||
|
min-width: 850px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, auto);
|
||||||
|
gap: auto;
|
||||||
|
padding: 50px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { Cell } from "interface";
|
||||||
|
import "./GameBoard.scss";
|
||||||
|
import BoardCell from "./BoardCell";
|
||||||
|
import { DieViewProps } from "../types/DieViewProps";
|
||||||
|
import { PlacePieceActionStack } from "../types/PlacePieceActionStack";
|
||||||
|
import { PlacePieceAction } from "../types/PlacePieceAction";
|
||||||
|
|
||||||
|
export interface GameBoardProps {
|
||||||
|
modifyDieState: (
|
||||||
|
matcher: (die: DieViewProps) => boolean,
|
||||||
|
newStateComputer: (die: DieViewProps) => Partial<DieViewProps>,
|
||||||
|
) => void;
|
||||||
|
fetchDie: (
|
||||||
|
matcher: (die: DieViewProps) => boolean,
|
||||||
|
) => DieViewProps | undefined;
|
||||||
|
setSpecialDieUsedInRound: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
refreshBoardRender: () => void;
|
||||||
|
board: Cell[][];
|
||||||
|
placePieceActionStack: PlacePieceActionStack;
|
||||||
|
setPlacePieceActionStack: React.Dispatch<
|
||||||
|
React.SetStateAction<PlacePieceActionStack>
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GameBoard = (props: GameBoardProps) => {
|
||||||
|
const { board, placePieceActionStack, setPlacePieceActionStack } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="game-board">
|
||||||
|
{board.flatMap((row, rowIndex) =>
|
||||||
|
row.map((cell, colIndex) => (
|
||||||
|
<BoardCell
|
||||||
|
{...props}
|
||||||
|
cell={cell}
|
||||||
|
placePieceActionHandler={(pieceId, rotation) => {
|
||||||
|
placePieceActionStack.executeAction(
|
||||||
|
new PlacePieceAction(pieceId, rotation, rowIndex, colIndex),
|
||||||
|
board,
|
||||||
|
);
|
||||||
|
setPlacePieceActionStack(placePieceActionStack);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GameBoard;
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { PieceId } from "interface";
|
||||||
|
|
||||||
|
export interface DieViewProps {
|
||||||
|
pieceId: PieceId;
|
||||||
|
isSelected: boolean;
|
||||||
|
isDisabled: boolean;
|
||||||
|
readonly isSpecial: boolean;
|
||||||
|
rotation: number;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { Cell, PieceId } from "interface";
|
||||||
|
|
||||||
|
export class PlacePieceAction {
|
||||||
|
pieceId: PieceId;
|
||||||
|
rotation: number;
|
||||||
|
cell: {
|
||||||
|
row: number;
|
||||||
|
col: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(pieceId: PieceId, rotation: number, row: number, col: number) {
|
||||||
|
this.pieceId = pieceId;
|
||||||
|
this.rotation = rotation;
|
||||||
|
this.cell = {
|
||||||
|
row: row,
|
||||||
|
col: col,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
do(board: Cell[][]) {
|
||||||
|
const cell = board[this.cell.row][this.cell.col];
|
||||||
|
cell.placePiece(this.pieceId, this.rotation as 0 | 90 | 180 | 270);
|
||||||
|
}
|
||||||
|
|
||||||
|
undo(board: Cell[][]) {
|
||||||
|
const cell = board[this.cell.row][this.cell.col];
|
||||||
|
if (!cell.placedPiece || cell.placedPiece.id !== this.pieceId) {
|
||||||
|
throw Error("Un-doing action error");
|
||||||
|
}
|
||||||
|
cell.removePiece();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { Cell } from "interface";
|
||||||
|
import { PlacePieceAction } from "./PlacePieceAction";
|
||||||
|
|
||||||
|
export class PlacePieceActionStack {
|
||||||
|
readonly committedActions: PlacePieceAction[] = [];
|
||||||
|
inProgressActions: PlacePieceAction[] = [];
|
||||||
|
|
||||||
|
executeAction(action: PlacePieceAction, board: Cell[][]) {
|
||||||
|
action.do(board);
|
||||||
|
this.inProgressActions.push(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
undoLastAction(board: Cell[][]) {
|
||||||
|
const lastAction = this.inProgressActions.pop();
|
||||||
|
if (!lastAction) {
|
||||||
|
throw Error("No in progress action to undo");
|
||||||
|
}
|
||||||
|
lastAction.undo(board);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetActions(board: Cell[][]) {
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
this.undoLastAction(board);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (
|
||||||
|
!(error instanceof Error) ||
|
||||||
|
error.message !== "No in progress action to undo"
|
||||||
|
) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
commitActions() {
|
||||||
|
this.committedActions.push(...this.inProgressActions);
|
||||||
|
this.inProgressActions = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
.landing-page {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
padding: 2%;
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
background: linear-gradient(-45deg, #03444a, #00a8a8, #f1bc52, #ff8f4b);
|
||||||
|
background-size: 400% 400%;
|
||||||
|
animation: gradient 15s ease infinite;
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
@keyframes gradient {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-page-body {
|
||||||
|
margin: auto;
|
||||||
|
width: 20%;
|
||||||
|
min-width: 300px;
|
||||||
|
|
||||||
|
#error-message {
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
padding: 2%;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-size: 1rem;
|
||||||
|
width: 70%;
|
||||||
|
margin-bottom: var(--pico-spacing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
import {
|
import {
|
||||||
|
attachHandlerToCreateLobbyError,
|
||||||
|
attachHandlerToJoinLobbyError,
|
||||||
CreateLobbyEvent,
|
CreateLobbyEvent,
|
||||||
|
ErrorCode,
|
||||||
handleCreateLobby,
|
handleCreateLobby,
|
||||||
handleJoinLobby,
|
handleJoinLobby,
|
||||||
JoinLobbyEvent,
|
JoinLobbyEvent,
|
||||||
} from "interface";
|
} from "interface";
|
||||||
import React, { ChangeEvent } from "react";
|
import { ChangeEvent, useEffect, useState } from "react";
|
||||||
import { Socket } from "socket.io-client";
|
import { Socket } from "socket.io-client";
|
||||||
|
import "./LandingPage.scss";
|
||||||
|
|
||||||
export interface LandingPageProps {
|
export interface LandingPageProps {
|
||||||
socket: Socket;
|
socket: Socket;
|
||||||
|
|
@ -14,42 +18,81 @@ export interface LandingPageProps {
|
||||||
const LandingPage = (props: LandingPageProps) => {
|
const LandingPage = (props: LandingPageProps) => {
|
||||||
const { socket } = props;
|
const { socket } = props;
|
||||||
|
|
||||||
const createLobbyPayload: CreateLobbyEvent = { userName: "" };
|
const [userName, setUsername] = useState("");
|
||||||
const joinLobbyPayload: JoinLobbyEvent = { userName: "", lobbyId: "" };
|
const [lobbyId, setLobbyId] = useState("");
|
||||||
const registerUsername = (event: ChangeEvent<HTMLInputElement>) => {
|
const createLobbyPayload: CreateLobbyEvent = { userName };
|
||||||
createLobbyPayload.userName = event.target.value;
|
const joinLobbyPayload: JoinLobbyEvent = { userName, lobbyId };
|
||||||
joinLobbyPayload.userName = event.target.value;
|
const registerUsername = (event: ChangeEvent<HTMLInputElement>) =>
|
||||||
};
|
setUsername(event.target.value);
|
||||||
const registerLobbyId = (event: ChangeEvent<HTMLInputElement>) =>
|
const registerLobbyId = (event: ChangeEvent<HTMLInputElement>) =>
|
||||||
(joinLobbyPayload.lobbyId = event.target.value);
|
setLobbyId(event.target.value);
|
||||||
|
|
||||||
|
const [receivedError, setReceivedError] = useState("" as ErrorCode);
|
||||||
|
const userNameErrorCodesToMessageMap = new Map([
|
||||||
|
[ErrorCode.MISSING_USER_NAME, "Introduce your player name"],
|
||||||
|
[
|
||||||
|
ErrorCode.INVALID_USER_NAME,
|
||||||
|
"Player name must be letters or spaces only and up to 25 characters",
|
||||||
|
],
|
||||||
|
[ErrorCode.DUPLICATE_USER_NAME, "Player name is already under use"],
|
||||||
|
]);
|
||||||
|
const userNameErrorMessage =
|
||||||
|
userNameErrorCodesToMessageMap.get(receivedError);
|
||||||
|
const gameCodeErrorCodesToMessageMap = new Map([
|
||||||
|
[ErrorCode.GAME_NOT_FOUND, "No game found with such lobby ID"],
|
||||||
|
[ErrorCode.MISSING_GAME_CODE, "Introduce a game code"],
|
||||||
|
]);
|
||||||
|
const gameCodeErrorMessage =
|
||||||
|
gameCodeErrorCodesToMessageMap.get(receivedError);
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
attachHandlerToCreateLobbyError(socket, (event) =>
|
||||||
|
setReceivedError(event.error),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
useEffect(() =>
|
||||||
|
attachHandlerToJoinLobbyError(socket, (event) =>
|
||||||
|
setReceivedError(event.error),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<div className="landing-page">
|
||||||
<div className="landing-page-title">
|
<div>
|
||||||
<h1>Trains And Roads</h1>
|
<h1>Trains And Roads</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="user-name-input">
|
<div className="landing-page-body">
|
||||||
<input placeholder="Enter username" onChange={registerUsername}></input>
|
<div>
|
||||||
</div>
|
<input
|
||||||
<div className="create-lobby-button">
|
placeholder="Enter username"
|
||||||
|
onChange={registerUsername}
|
||||||
|
maxLength={20}
|
||||||
|
></input>
|
||||||
|
{userNameErrorMessage && (
|
||||||
|
<small id="error-message">{userNameErrorMessage}</small>
|
||||||
|
)}
|
||||||
<button onClick={() => handleCreateLobby(socket, createLobbyPayload)}>
|
<button onClick={() => handleCreateLobby(socket, createLobbyPayload)}>
|
||||||
Create Lobby
|
Create Lobby
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="join-lobby">
|
<div>
|
||||||
<input
|
<input
|
||||||
className="lobby-id-input"
|
|
||||||
placeholder="Enter Lobby Id"
|
placeholder="Enter Lobby Id"
|
||||||
onChange={registerLobbyId}
|
onChange={registerLobbyId}
|
||||||
|
maxLength={5}
|
||||||
></input>
|
></input>
|
||||||
|
{gameCodeErrorMessage && (
|
||||||
|
<small id="error-message">{gameCodeErrorMessage}</small>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
className="join-lobby-button secondary"
|
className="secondary"
|
||||||
onClick={() => handleJoinLobby(socket, joinLobbyPayload)}
|
onClick={() => handleJoinLobby(socket, joinLobbyPayload)}
|
||||||
>
|
>
|
||||||
Join Lobby
|
Join Lobby
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default LandingPage;
|
export default LandingPage;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
.lobby-page {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
padding: 2%;
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
background: linear-gradient(-45deg, #03444a, #00a8a8, #f1bc52, #ff8f4b);
|
||||||
|
background-size: 400% 400%;
|
||||||
|
animation: gradient 15s ease infinite;
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
@keyframes gradient {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lobby-page-body {
|
||||||
|
margin: auto;
|
||||||
|
width: 20%;
|
||||||
|
min-width: 300px;
|
||||||
|
|
||||||
|
.lobby-user-name {
|
||||||
|
margin: 0 auto 10px;
|
||||||
|
padding: 10px;
|
||||||
|
width: 60%;
|
||||||
|
border: 2px solid cyan;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-code {
|
||||||
|
margin: 0 auto 10px;
|
||||||
|
padding: 10px;
|
||||||
|
width: 60%;
|
||||||
|
border: 2px solid blue;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-game-button {
|
||||||
|
margin: 30px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import "./LobbyPage.scss";
|
||||||
|
import { Socket } from "socket.io-client";
|
||||||
|
import { attachHandlerToUpdateLobbyEvent, ClientEvent } from "interface";
|
||||||
|
|
||||||
|
export interface LobbyPageProps {
|
||||||
|
initialPlayerNames: string[];
|
||||||
|
gameCode: string;
|
||||||
|
socket: Socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LobbyPage = (props: LobbyPageProps) => {
|
||||||
|
const { initialPlayerNames, gameCode, socket } = props;
|
||||||
|
const [playerNames, setPlayerNames] = useState(initialPlayerNames);
|
||||||
|
|
||||||
|
attachHandlerToUpdateLobbyEvent(socket, (event) => {
|
||||||
|
setPlayerNames(event.playerNames);
|
||||||
|
});
|
||||||
|
const startGame = () => socket.emit(ClientEvent.START_GAME);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="lobby-page">
|
||||||
|
<div className="landing-page-title">
|
||||||
|
<h1>Trains And Roads</h1>
|
||||||
|
</div>
|
||||||
|
<div className="lobby-page-body">
|
||||||
|
{playerNames.map((name) => (
|
||||||
|
<article className="lobby-user-name">{name}</article>
|
||||||
|
))}
|
||||||
|
<div className="start-game-button">
|
||||||
|
<button onClick={startGame}>Start Game</button>
|
||||||
|
</div>
|
||||||
|
<article className="game-code">Game code: {gameCode}</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LobbyPage;
|
||||||