Compare commits
50 Commits
micosil-te
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
10949f30df | |
|
|
09bd13f772 | |
|
|
0b5684f2b9 | |
|
|
7c4a6f5603 | |
|
|
0a0ca65e3b | |
|
|
86d5137857 | |
|
|
b72a74789c | |
|
|
45cbf885a3 | |
|
|
00c365de84 | |
|
|
4f9950c6a9 | |
|
|
98e62180dc | |
|
|
2191a2fdc4 | |
|
|
fae67ce2a2 | |
|
|
3af9caff9a | |
|
|
5fc02aa6a6 | |
|
|
035b7da335 | |
|
|
b1d57ebfcd | |
|
|
1ab3db8f67 | |
|
|
e7beb85302 | |
|
|
835ce2c62a | |
|
|
39d1ec9a77 | |
|
|
406e4d2f75 | |
|
|
7354586ef5 | |
|
|
163ca86734 | |
|
|
af133bae8b | |
|
|
d72984851e | |
|
|
0945336463 | |
|
|
88627bb9a7 | |
|
|
e9305893cb | |
|
|
4497a0a2f2 | |
|
|
c75b3ef1c2 | |
|
|
6a222640be | |
|
|
d3ecc52cf6 | |
|
|
4b22c5ead8 | |
|
|
0fc8a7383f | |
|
|
d78b47fb07 | |
|
|
695ae069d5 | |
|
|
a1eaab6534 | |
|
|
ec6526deb5 | |
|
|
56705d3574 | |
|
|
250325d8d2 | |
|
|
d906cf486b | |
|
|
97de40a396 | |
|
|
391a1c91a7 | |
|
|
3f77a32e70 | |
|
|
03c51e1d34 | |
|
|
dc24f62f64 | |
|
|
f1c284915e | |
|
|
3f49d880c8 | |
|
|
e7447436a6 |
|
|
@ -142,6 +142,7 @@ dist
|
|||
|
||||
# production
|
||||
/build
|
||||
*/build/
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/platform-socket.io": "^10.4.7",
|
||||
"@nestjs/websockets": "^10.4.7",
|
||||
"interface": "file:../interface",
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
|
|
@ -32,6 +33,7 @@
|
|||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"jest": "^29.5.0",
|
||||
"prettier": "^3.0.0",
|
||||
"socket.io": "^4.8.1",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.1.0",
|
||||
|
|
@ -41,6 +43,27 @@
|
|||
"typescript": "^5.1.3"
|
||||
}
|
||||
},
|
||||
"../interface": {
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"socket.io": "^4.8.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"uuid": "^11.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^8.0.0",
|
||||
"@types/node": "^22.9.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
||||
"@typescript-eslint/parser": "^8.15.0",
|
||||
"eslint": "^8.0.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"globals": "^15.12.0",
|
||||
"typescript": "^5.6.3",
|
||||
"typescript-eslint": "^8.15.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||
|
|
@ -1716,6 +1739,24 @@
|
|||
"rxjs": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/platform-socket.io/node_modules/socket.io": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz",
|
||||
"integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "~2.0.0",
|
||||
"cors": "~2.8.5",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io": "~6.6.0",
|
||||
"socket.io-adapter": "~2.5.2",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/schematics": {
|
||||
"version": "10.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.2.3.tgz",
|
||||
|
|
@ -5271,6 +5312,10 @@
|
|||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/interface": {
|
||||
"resolved": "../interface",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
|
|
@ -7995,9 +8040,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/socket.io": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz",
|
||||
"integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==",
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
|
||||
"integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.4",
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@
|
|||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"build": "npm run lint && nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start --watch",
|
||||
"start": "npm run build && nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"lint": "eslint \"src/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
|
|
@ -24,6 +24,7 @@
|
|||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/platform-socket.io": "^10.4.7",
|
||||
"@nestjs/websockets": "^10.4.7",
|
||||
"interface": "file:../interface",
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
|
|
@ -42,6 +43,7 @@
|
|||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"jest": "^29.5.0",
|
||||
"prettier": "^3.0.0",
|
||||
"socket.io": "^4.8.1",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.1.0",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { AppService } from './app.service';
|
||||
import { PlayerService } from './players/player.service';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
providers: [AppService],
|
||||
providers: [AppService, PlayerService],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,41 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { MessageBody, SubscribeMessage, WebSocketGateway } from '@nestjs/websockets';
|
||||
import {
|
||||
ConnectedSocket,
|
||||
OnGatewayConnection,
|
||||
MessageBody,
|
||||
SubscribeMessage,
|
||||
WebSocketGateway,
|
||||
} from '@nestjs/websockets';
|
||||
import { PlayerService } from './players/player.service';
|
||||
import { Socket } from 'socket.io';
|
||||
import { ClientEvent, CreateLobbyEvent, JoinLobbyEvent } from 'interface';
|
||||
|
||||
@WebSocketGateway({ cors: true })
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
export class AppService implements OnGatewayConnection {
|
||||
private readonly logger = new Logger(AppService.name);
|
||||
|
||||
@SubscribeMessage('example-request')
|
||||
handleCustomEvent(@MessageBody() data: string): unknown {
|
||||
this.logger.debug(`Received request in backend with data: ${data}`);
|
||||
return {event: "example-response", data: `Replying from backend, received data: ${data}`};
|
||||
constructor(private readonly playerService: PlayerService) {}
|
||||
handleConnection(client: Socket) {
|
||||
this.playerService.createPlayer(client.id);
|
||||
this.logger.log(client.id);
|
||||
}
|
||||
|
||||
@SubscribeMessage(ClientEvent.CREATE_LOBBY)
|
||||
handleCreateLobby(
|
||||
@ConnectedSocket() client: Socket,
|
||||
@MessageBody() event: CreateLobbyEvent,
|
||||
) {
|
||||
this.playerService.addUserName(client.id, event.userName);
|
||||
this.logger.log('Se ha creado un lobby');
|
||||
}
|
||||
|
||||
@SubscribeMessage(ClientEvent.JOIN_LOBBY)
|
||||
handleJoinLobby(
|
||||
@ConnectedSocket() client: Socket,
|
||||
@MessageBody() event: JoinLobbyEvent,
|
||||
) {
|
||||
this.playerService.addUserName(client.id, event.userName);
|
||||
this.logger.log('Te has unido a un lobby');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { AppModule } from './app.module';
|
|||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
await app.listen(process.env.PORT ?? 3010);
|
||||
const port = process.env.PORT ?? 3010;
|
||||
await app.listen(port);
|
||||
}
|
||||
bootstrap();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Player } from './player';
|
||||
|
||||
@Injectable()
|
||||
export class PlayerService {
|
||||
private readonly logger = new Logger(PlayerService.name);
|
||||
private readonly players: Map<string, Player> = new Map();
|
||||
|
||||
createPlayer(socketId: string) {
|
||||
const player: Player = new Player(socketId);
|
||||
this.players.set(socketId, player);
|
||||
this.logger.log([...this.players.entries()]);
|
||||
}
|
||||
|
||||
addUserName(socketId: string, userName: string) {
|
||||
this.players.get(socketId).userName = userName;
|
||||
this.logger.log([...this.players.entries()]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
export class Player {
|
||||
socketId: string;
|
||||
userName?: string;
|
||||
constructor(socketId: string) {
|
||||
this.socketId = socketId;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
module.exports = {
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: "tsconfig.json",
|
||||
tsconfigRootDir: __dirname,
|
||||
sourceType: "module",
|
||||
},
|
||||
plugins: ["@typescript-eslint/eslint-plugin"],
|
||||
extends: ["plugin:@typescript-eslint/strict", "plugin:prettier/recommended"],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
ignorePatterns: [".eslintrc.js", "dist/"],
|
||||
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,5 @@
|
|||
export enum ClientEvent {
|
||||
CREATE_LOBBY = "create-lobby",
|
||||
JOIN_LOBBY = "join-lobby",
|
||||
START_GAME = "start-game",
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { Socket } from "socket.io-client";
|
||||
import { ClientEvent } from "./ClientEvent";
|
||||
|
||||
export type CreateLobbyEvent = {
|
||||
userName: string;
|
||||
};
|
||||
|
||||
export function handleCreateLobby(
|
||||
socket: Socket,
|
||||
payload: CreateLobbyEvent,
|
||||
): void {
|
||||
socket.emit(ClientEvent.CREATE_LOBBY, payload);
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { Socket } from "socket.io-client";
|
||||
import { ClientEvent } from "./ClientEvent";
|
||||
|
||||
export type JoinLobbyEvent = {
|
||||
userName: string;
|
||||
lobbyId: string;
|
||||
};
|
||||
|
||||
export function handleJoinLobby(socket: Socket, payload: JoinLobbyEvent): void {
|
||||
socket.emit(ClientEvent.JOIN_LOBBY, payload);
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export enum TrackType {
|
||||
RAIL = "RAIL",
|
||||
ROAD = "ROAD",
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
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 "./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";
|
||||
export * from "./server-events/ServerError";
|
||||
export * from "./server-events/ServerEvent";
|
||||
export * from "./server-events/StartRoundEvent";
|
||||
export * from "./server-events/UpdateLobbyEvent";
|
||||
export * from "./client-events/ClientEvent";
|
||||
export * from "./client-events/CreateLobbyEvent";
|
||||
export * from "./client-events/JoinLobbyEvent";
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "interface",
|
||||
"version": "1.0.0",
|
||||
"description": "A collection of common functions for the trains and roads project.",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "npm run lint && tsc",
|
||||
"lint": "eslint \"*.ts\" \"*/**/*.ts\" --fix"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^8.0.0",
|
||||
"@types/node": "^22.9.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
||||
"@typescript-eslint/parser": "^8.15.0",
|
||||
"eslint": "^8.0.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"globals": "^15.12.0",
|
||||
"typescript": "^5.6.3",
|
||||
"typescript-eslint": "^8.15.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"uuid": "^11.0.3",
|
||||
"socket.io": "^4.8.1",
|
||||
"socket.io-client": "^4.8.1"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export enum ServerError {
|
||||
CREATE_LOBBY_ERROR = "create-lobby-error",
|
||||
JOIN_LOBBY_ERROR = "join-lobby-error",
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export enum ServerEvent {
|
||||
LOBBY_UPDATE = "lobby-update",
|
||||
START_ROUND = "start-round",
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { Socket as ServerSocket } from "socket.io";
|
||||
import { Socket as ClientSocket } from "socket.io-client";
|
||||
import { ServerEvent } from "./ServerEvent";
|
||||
|
||||
export type UpdateLobbyEvent = {
|
||||
playerNames: Array<string>;
|
||||
gameCode: string;
|
||||
};
|
||||
|
||||
export const emitUpdateLobbyEvent = (
|
||||
socket: ServerSocket,
|
||||
payload: UpdateLobbyEvent,
|
||||
) => {
|
||||
socket.emit(ServerEvent.LOBBY_UPDATE, payload);
|
||||
};
|
||||
|
||||
export function attachHandlerToUpdateLobbyEvent(
|
||||
socket: ClientSocket,
|
||||
handler: (event: UpdateLobbyEvent) => void,
|
||||
): void {
|
||||
socket.once(ServerEvent.LOBBY_UPDATE, handler);
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
"experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||
"emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs", /* Specify what module code is generated. */
|
||||
"rootDir": "./", /* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
"declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./dist", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
module.exports = {
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: "tsconfig.json",
|
||||
tsconfigRootDir: __dirname,
|
||||
sourceType: "module",
|
||||
},
|
||||
plugins: ["@typescript-eslint/eslint-plugin"],
|
||||
extends: ["plugin:@typescript-eslint/strict", "plugin:prettier/recommended"],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
ignorePatterns: [".eslintrc.js", "dist/", "node_modules/", "build/"],
|
||||
rules: {
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
endOfLine: "auto",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
@ -3,13 +3,15 @@
|
|||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@picocss/pico": "^2.0.6",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.119",
|
||||
"@types/node": "^17.0.29",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"interface": "file:../interface",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-scripts": "5.0.1",
|
||||
|
|
@ -20,9 +22,10 @@
|
|||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"build": "npm run lint && react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
"eject": "react-scripts eject",
|
||||
"lint": "eslint \"*/**/*.tsx\" \"*/**/*.ts\" --fix"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
|
|
@ -41,5 +44,13 @@
|
|||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.15.0",
|
||||
"eslint": "^8.0.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"typescript-eslint": "^8.15.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
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,12 +1,3 @@
|
|||
body {
|
||||
font-size: 20px;
|
||||
|
||||
header > button {
|
||||
background-color: aliceblue;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid black;
|
||||
border-radius: 10%;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@use "node_modules/@picocss/pico/scss/pico" with (
|
||||
$theme-color: "pumpkin"
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('Renders hello world', () => {
|
||||
render(<App />);
|
||||
expect(screen.getByText("Hello World! Front")).toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -1,21 +1,57 @@
|
|||
import React from 'react';
|
||||
import { io } from "socket.io-client";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { Socket } from "socket.io-client";
|
||||
import LandingPage from "./pages/landing/LandingPage";
|
||||
import GamePage from "./pages/game/GamePage";
|
||||
import LobbyPage from "./pages/lobby/LobbyPage";
|
||||
import {
|
||||
attachHandlerToStartRoundEvent,
|
||||
attachHandlerToUpdateLobbyEvent,
|
||||
PieceId,
|
||||
} from "interface";
|
||||
|
||||
function App() {
|
||||
const socket = io("http://localhost:3010");
|
||||
const emitData = () => console.log(socket.emit("example-request", "custom-request"));
|
||||
socket.on("example-response", (data) => console.log(`Received response in front end with data: ${data}`));
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<button onClick={emitData}>
|
||||
Emit Data
|
||||
</button>
|
||||
Hello World! Front
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
export interface AppProps {
|
||||
socket: Socket;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="app">
|
||||
{renderedPage === "LANDING" && <LandingPage {...props} />}
|
||||
{renderedPage === "LOBBY" && (
|
||||
<LobbyPage
|
||||
{...props}
|
||||
initialPlayerNames={initialPlayerNames}
|
||||
gameCode={gameCode}
|
||||
/>
|
||||
)}
|
||||
{renderedPage === "GAME" && (
|
||||
<GamePage initialPieceIds={initialPieceIds} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import './App.scss'
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import "./App.scss";
|
||||
import { io } from "socket.io-client";
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
document.getElementById("root") as HTMLElement,
|
||||
);
|
||||
const socket = io("http://localhost:3010");
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
<App socket={socket} />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,55 @@
|
|||
import {
|
||||
CreateLobbyEvent,
|
||||
handleCreateLobby,
|
||||
handleJoinLobby,
|
||||
JoinLobbyEvent,
|
||||
} from "interface";
|
||||
import React, { ChangeEvent } from "react";
|
||||
import { Socket } from "socket.io-client";
|
||||
|
||||
export interface LandingPageProps {
|
||||
socket: Socket;
|
||||
}
|
||||
|
||||
const LandingPage = (props: LandingPageProps) => {
|
||||
const { socket } = props;
|
||||
|
||||
const createLobbyPayload: CreateLobbyEvent = { userName: "" };
|
||||
const joinLobbyPayload: JoinLobbyEvent = { userName: "", lobbyId: "" };
|
||||
const registerUsername = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
createLobbyPayload.userName = event.target.value;
|
||||
joinLobbyPayload.userName = event.target.value;
|
||||
};
|
||||
const registerLobbyId = (event: ChangeEvent<HTMLInputElement>) =>
|
||||
(joinLobbyPayload.lobbyId = event.target.value);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="landing-page-title">
|
||||
<h1>Trains And Roads</h1>
|
||||
</div>
|
||||
<div className="user-name-input">
|
||||
<input placeholder="Enter username" onChange={registerUsername}></input>
|
||||
</div>
|
||||
<div className="create-lobby-button">
|
||||
<button onClick={() => handleCreateLobby(socket, createLobbyPayload)}>
|
||||
Create Lobby
|
||||
</button>
|
||||
</div>
|
||||
<div className="join-lobby">
|
||||
<input
|
||||
className="lobby-id-input"
|
||||
placeholder="Enter Lobby Id"
|
||||
onChange={registerLobbyId}
|
||||
></input>
|
||||
<button
|
||||
className="join-lobby-button secondary"
|
||||
onClick={() => handleJoinLobby(socket, joinLobbyPayload)}
|
||||
>
|
||||
Join Lobby
|
||||
</button>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
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;
|
||||
|
|
@ -2,4 +2,4 @@
|
|||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
import "@testing-library/jest-dom";
|
||||
|
|
|
|||