From 1ab3db8f671553064436801823811a2048272952 Mon Sep 17 00:00:00 2001 From: MiguelMLorente Date: Tue, 3 Dec 2024 12:29:10 +0100 Subject: [PATCH 1/4] Enable placing pieces in the game board --- interface/types/Cell.ts | 14 ++ interface/types/Piece.ts | 5 +- web/src/pages/game/GamePage.tsx | 22 +++- web/src/pages/game/components/BoardCell.scss | 129 +++++++++++++++++++ web/src/pages/game/components/BoardCell.tsx | 68 ++++++++++ web/src/pages/game/components/DiceSet.scss | 11 -- web/src/pages/game/components/DiceSet.tsx | 44 +++++-- web/src/pages/game/components/Die.scss | 18 +++ web/src/pages/game/components/Die.tsx | 27 ++++ web/src/pages/game/components/GameBoard.scss | 117 ----------------- web/src/pages/game/components/GameBoard.tsx | 52 +++++--- 11 files changed, 345 insertions(+), 162 deletions(-) create mode 100644 web/src/pages/game/components/BoardCell.scss create mode 100644 web/src/pages/game/components/BoardCell.tsx create mode 100644 web/src/pages/game/components/Die.scss create mode 100644 web/src/pages/game/components/Die.tsx diff --git a/interface/types/Cell.ts b/interface/types/Cell.ts index 8dc8236..757e471 100644 --- a/interface/types/Cell.ts +++ b/interface/types/Cell.ts @@ -1,10 +1,16 @@ import { CellType } from "../constants/CellType"; import { Direction } from "../constants/Direction"; +import { PieceId, pieceMap } from "../constants/Pieces"; import { ExternalNode } from "./ExternalNode"; +import { PlacedPiece } from "./PlacedPiece"; export class Cell { public readonly externalNodes: Map; public readonly cellType: CellType; + public placedPiece?: { + piece: PlacedPiece; + id: PieceId; + }; constructor(cellType: CellType) { this.externalNodes = new Map([ @@ -21,4 +27,12 @@ export class Cell { if (!node) throw Error(`Could not find node at ${direction}`); return node!; } + + public placePiece(pieceId: PieceId) { + if (this.placedPiece !== undefined) return; + this.placedPiece = { + piece: pieceMap[pieceId].toPlacedPiece(this), + id: pieceId, + }; + } } diff --git a/interface/types/Piece.ts b/interface/types/Piece.ts index dae249a..f684cf4 100644 --- a/interface/types/Piece.ts +++ b/interface/types/Piece.ts @@ -3,7 +3,6 @@ import { TrackType } from "../constants/TrackType"; import { Cell } from "./Cell"; import { InternalNode } from "./InternalNode"; import { PlacedPiece } from "./PlacedPiece"; -import { Node } from "./Node"; import { InternalNodeType } from "../constants/InternalNodeType"; export interface PieceProps { @@ -74,8 +73,8 @@ export class Piece { nodes: { firstNode: cell.getNodeAt(track.joinedPoints.firstPoint), secondNode: - track.joinedPoints.secondPoint instanceof Node - ? (track.joinedPoints.secondPoint as Node) + track.joinedPoints.secondPoint instanceof InternalNode + ? (track.joinedPoints.secondPoint as InternalNode) : cell.getNodeAt(track.joinedPoints.secondPoint as Direction), }, type: track.type, diff --git a/web/src/pages/game/GamePage.tsx b/web/src/pages/game/GamePage.tsx index 3b42ea0..499bd51 100644 --- a/web/src/pages/game/GamePage.tsx +++ b/web/src/pages/game/GamePage.tsx @@ -1,16 +1,32 @@ -import React from "react"; +import React, { useState } from "react"; import GameBoard from "./components/GameBoard"; import DiceSet from "./components/DiceSet"; import "./GamePage.scss"; +import { PieceId } from "interface"; const GamePage = () => { + const getRandomPieceId = () => { + const randomId = Math.floor(Math.random() * Object.keys(PieceId).length); + return Object.values(PieceId)[randomId]; + }; + + const [dice, setDice] = useState( + [1, 2, 3, 4].map(() => { + return { + pieceId: getRandomPieceId(), + isSelected: false, + isDisabled: false, + }; + }), + ); + return (

Game Page Title

- +
- +
diff --git a/web/src/pages/game/components/BoardCell.scss b/web/src/pages/game/components/BoardCell.scss new file mode 100644 index 0000000..a7822ed --- /dev/null +++ b/web/src/pages/game/components/BoardCell.scss @@ -0,0 +1,129 @@ +.cell { + width: 100px; + height: 100px; + border: 2px solid black; + border-radius: 10%; + + .piece { + border-radius: 10%; + position: relative; + + &:nth-child(2) { + top: -20px; + } + + &:nth-child(3) { + top: -40px; + } + } + + &.house { + background-color: blue; + } + + &.university { + background-color: orange; + } + + &.factory { + background-color: grey; + } + + .exit: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)); + } + } + + .exit: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)); + } + } + + .exit: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)); + } + } + + .exit: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%)); + } + } +} + +.exit { + position: relative; + width: 20px; + height: 20px; + + &:has(.exit-north) { + left: 50%; + transform: translate(calc(-50% - 1px), -100%); + } + + &:has(.exit-south) { + left: 50%; + top: 100%; + transform: translate(calc(-50% - 1px), 0%); + } + + &:has(.exit-east) { + left: 100%; + top: 50%; + transform: rotate(90deg) translate(-50%, 0); + } + + &:has(.exit-west) { + left: 0%; + top: 50%; + transform: rotate(90deg) translate(-50%, 100%); + } + + &:has(.exit-north), &:has(.exit-south) { + + .exit:has(.exit-east) { + left: 100%; + transform: rotate(90deg) translate(-150%, 0); + } + } + + &:has(.exit-north), &:has(.exit-south) { + + .exit:has(.exit-west) { + top: 50%; + transform: rotate(90deg) translate(-150%, 100%); + } + } +} \ No newline at end of file diff --git a/web/src/pages/game/components/BoardCell.tsx b/web/src/pages/game/components/BoardCell.tsx new file mode 100644 index 0000000..2e1d5af --- /dev/null +++ b/web/src/pages/game/components/BoardCell.tsx @@ -0,0 +1,68 @@ +import { Cell, directions, Exit, PieceId } from "interface"; +import "./BoardCell.scss"; + +export interface BoardCellProps { + cell: Cell; + dice: { + pieceId: PieceId; + isSelected: boolean; + isDisabled: boolean; + }[]; + setDice: React.Dispatch< + React.SetStateAction< + { + pieceId: PieceId; + isSelected: boolean; + isDisabled: boolean; + }[] + > + >; + refreshBoardRender: () => void; +} + +const BoardCell = (props: BoardCellProps) => { + const { cell, dice, setDice, refreshBoardRender } = props; + const handleBoardCellClick = () => { + const selectedDie = dice.find((die) => die.isSelected); + if (!selectedDie) return; + cell.placePiece(selectedDie.pieceId); + setDice( + dice.map((die) => { + return die !== selectedDie + ? die + : { + pieceId: selectedDie.pieceId, + isSelected: false, + isDisabled: true, + }; + }), + ); + refreshBoardRender(); + }; + + return ( +
+ {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 ( +
+
+
+ ); + })} + {cell.placedPiece && ( + + )} +
+ ); +}; + +export default BoardCell; diff --git a/web/src/pages/game/components/DiceSet.scss b/web/src/pages/game/components/DiceSet.scss index 22145b0..1152b08 100644 --- a/web/src/pages/game/components/DiceSet.scss +++ b/web/src/pages/game/components/DiceSet.scss @@ -3,15 +3,4 @@ grid-template-columns: repeat(2, auto); gap: 20px; padding: 50px; - - .dice { - height: 150px; - width: 150px; - border: 2px solid black; - border-radius: 10%; - - img{ - border-radius: 10%; - } - } } \ No newline at end of file diff --git a/web/src/pages/game/components/DiceSet.tsx b/web/src/pages/game/components/DiceSet.tsx index c81e7d1..8e10fe0 100644 --- a/web/src/pages/game/components/DiceSet.tsx +++ b/web/src/pages/game/components/DiceSet.tsx @@ -1,19 +1,45 @@ import { PieceId } from "interface"; import "./DiceSet.scss"; +import Die from "./Die"; -const DiceSet = () => { - const getRandomPieceId = () => { - const randomId = Math.floor(Math.random() * Object.keys(PieceId).length); - return Object.values(PieceId)[randomId]; +export interface DiceSetProps { + dice: { + pieceId: PieceId; + isSelected: boolean; + isDisabled: boolean; + }[]; + setDice: React.Dispatch< + React.SetStateAction< + { + pieceId: PieceId; + isSelected: boolean; + isDisabled: boolean; + }[] + > + >; +} +const DiceSet = (props: DiceSetProps) => { + const { dice, setDice } = props; + const handleDieClick = (die: { + pieceId: PieceId; + isSelected: boolean; + isDisabled: boolean; + }) => { + if (die.isDisabled) return; + const newDiceState = dice.map((oldDie) => { + const isSelected = die === oldDie; + return { + ...oldDie, + isSelected: isSelected, + }; + }); + setDice(newDiceState); }; - const displayedPieceIds = [1, 2, 3, 4].map(getRandomPieceId); return (
- {displayedPieceIds.map((pieceId) => ( -
- -
+ {dice.map((die) => ( + ))}
); diff --git a/web/src/pages/game/components/Die.scss b/web/src/pages/game/components/Die.scss new file mode 100644 index 0000000..d96f7f8 --- /dev/null +++ b/web/src/pages/game/components/Die.scss @@ -0,0 +1,18 @@ +.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; + } +} \ No newline at end of file diff --git a/web/src/pages/game/components/Die.tsx b/web/src/pages/game/components/Die.tsx new file mode 100644 index 0000000..f6644b0 --- /dev/null +++ b/web/src/pages/game/components/Die.tsx @@ -0,0 +1,27 @@ +import { PieceId } from "interface"; +import "./Die.scss"; + +interface DieProps { + die: { pieceId: PieceId; isSelected: boolean; isDisabled: boolean }; + handleDieClick: (die: { + pieceId: PieceId; + isSelected: boolean; + isDisabled: boolean; + }) => void; +} + +const Die = (props: DieProps) => { + const { die, handleDieClick } = props; + const { pieceId, isSelected, isDisabled } = die; + + return ( +
handleDieClick(die)} + > + +
+ ); +}; + +export default Die; diff --git a/web/src/pages/game/components/GameBoard.scss b/web/src/pages/game/components/GameBoard.scss index 4d6cc7b..a11648f 100644 --- a/web/src/pages/game/components/GameBoard.scss +++ b/web/src/pages/game/components/GameBoard.scss @@ -7,121 +7,4 @@ grid-template-columns: repeat(7, auto); gap: auto; padding: 50px; - - .cell { - width: 100px; - height: 100px; - border: 2px solid black; - border-radius: 10%; - - &.house { - background-color: blue; - } - - &.university { - background-color: orange; - } - - &.factory { - background-color: grey; - } - - .exit: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)); - } - } - - .exit: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)); - } - } - - .exit: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)); - } - } - - .exit: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%)); - } - } - } } - -.exit { - position: relative; - width: 20px; - height: 20px; - - &:has(.exit-north) { - left: 50%; - transform: translate(calc(-50% - 1px), -100%); - } - - &:has(.exit-south) { - left: 50%; - top: 100%; - transform: translate(calc(-50% - 1px), 0%); - } - - &:has(.exit-east) { - left: 100%; - top: 50%; - transform: rotate(90deg) translate(-50%, 0); - } - - &:has(.exit-west) { - left: 0%; - top: 50%; - transform: rotate(90deg) translate(-50%, 100%); - } - - &:has(.exit-north), &:has(.exit-south) { - + .exit:has(.exit-east) { - left: 100%; - transform: rotate(90deg) translate(-150%, 0); - } - } - - &:has(.exit-north), &:has(.exit-south) { - + .exit:has(.exit-west) { - top: 50%; - transform: rotate(90deg) translate(-150%, 100%); - } - } -} \ No newline at end of file diff --git a/web/src/pages/game/components/GameBoard.tsx b/web/src/pages/game/components/GameBoard.tsx index e3dabf0..a713e7d 100644 --- a/web/src/pages/game/components/GameBoard.tsx +++ b/web/src/pages/game/components/GameBoard.tsx @@ -1,28 +1,42 @@ -import { buildBoard, Cell, directions, Exit } from "interface"; +import { buildBoard, PieceId } from "interface"; import "./GameBoard.scss"; +import { useState } from "react"; +import BoardCell from "./BoardCell"; -const GameBoard = () => { - const board: Cell[][] = buildBoard(); +export interface GameBoardProps { + dice: { + pieceId: PieceId; + isSelected: boolean; + isDisabled: boolean; + }[]; + setDice: React.Dispatch< + React.SetStateAction< + { + pieceId: PieceId; + isSelected: boolean; + isDisabled: boolean; + }[] + > + >; +} + +const GameBoard = (props: GameBoardProps) => { + const [board, setBoard] = useState(buildBoard()); + const [id, setId] = useState(1); + const refreshBoardRender = () => { + setBoard(board); + setId(id + 1); + }; return ( -
+
{board.flatMap((row) => row.map((cell) => ( -
- {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 ( -
-
-
- ); - })} -
+ )), )}
From b1d57ebfcdefa82bd95453c78ee4dbb16dea783c Mon Sep 17 00:00:00 2001 From: MiguelMLorente Date: Tue, 3 Dec 2024 13:46:52 +0100 Subject: [PATCH 2/4] Disallow placing piece in places with invalid adjacent connections --- interface/index.ts | 1 - interface/types/Cell.ts | 41 ++++++++++++++++++++- interface/types/Node.ts | 4 -- interface/types/PlacedPiece.ts | 22 ++++++++--- web/src/pages/game/components/BoardCell.tsx | 7 +++- 5 files changed, 63 insertions(+), 12 deletions(-) delete mode 100644 interface/types/Node.ts diff --git a/interface/index.ts b/interface/index.ts index 0cf1028..66f3650 100644 --- a/interface/index.ts +++ b/interface/index.ts @@ -11,5 +11,4 @@ export * from "./types/ExternalNode"; export * from "./types/InternalNode"; export * from "./types/Piece"; export * from "./types/PlacedPiece"; -export * from "./types/Node"; export * from "./BoardBuilder"; diff --git a/interface/types/Cell.ts b/interface/types/Cell.ts index 757e471..9f79fad 100644 --- a/interface/types/Cell.ts +++ b/interface/types/Cell.ts @@ -1,7 +1,10 @@ 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 { PlacedPiece } from "./PlacedPiece"; export class Cell { @@ -30,8 +33,44 @@ export class Cell { public placePiece(pieceId: PieceId) { if (this.placedPiece !== undefined) return; + const piece = pieceMap[pieceId]; + const connectionTypes = Array.from(piece.tracks).flatMap((track) => { + const directions = + track.joinedPoints.secondPoint instanceof InternalNode + ? [track.joinedPoints.firstPoint] + : [track.joinedPoints.firstPoint, track.joinedPoints.secondPoint]; + return directions.map((direction) => { + return { direction: direction, type: track.type }; + }); + }); + connectionTypes.forEach(({ direction, type }) => { + const adjacentPoint = this.getNodeAt(direction).traverseBorder(); + if (adjacentPoint instanceof Exit) { + if ( + adjacentPoint.type !== ExitType.AMBIVALENT && + adjacentPoint.type.toString() !== type.toString() + ) { + throw Error( + `Unable to place piece, invalid border at direction ${direction}`, + ); + } + } else { + if (adjacentPoint.cell.placedPiece !== undefined) { + const adjacentTrack = + adjacentPoint.cell.placedPiece.piece.findTrackForDirection( + adjacentPoint.direction, + ); + if (adjacentTrack !== undefined && adjacentTrack.type !== type) { + throw Error( + `Unable to place piece next to another due to conflicting track types at ${direction}`, + ); + } + } + } + }); + this.placedPiece = { - piece: pieceMap[pieceId].toPlacedPiece(this), + piece: piece.toPlacedPiece(this), id: pieceId, }; } diff --git a/interface/types/Node.ts b/interface/types/Node.ts deleted file mode 100644 index fa5bbe0..0000000 --- a/interface/types/Node.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { ExternalNode } from "./ExternalNode"; -import { InternalNode } from "./InternalNode"; - -export type Node = ExternalNode | InternalNode; diff --git a/interface/types/PlacedPiece.ts b/interface/types/PlacedPiece.ts index 7582a42..cdbc1f0 100644 --- a/interface/types/PlacedPiece.ts +++ b/interface/types/PlacedPiece.ts @@ -1,22 +1,25 @@ +import { Direction } from "../constants/Direction"; import { TrackType } from "../constants/TrackType"; import { Cell } from "./Cell"; +import { ExternalNode } from "./ExternalNode"; import { InternalNode } from "./InternalNode"; -import { Node } from "./Node"; export class PlacedPiece { tracks: Set<{ nodes: { - firstNode: Node; - secondNode: Node; + firstNode: ExternalNode; + secondNode: ExternalNode | InternalNode; }; type: TrackType; }>; internalNode?: InternalNode; cell: Cell; - constructor( tracks: Set<{ - nodes: { firstNode: Node; secondNode: Node }; + nodes: { + firstNode: ExternalNode; + secondNode: ExternalNode | InternalNode; + }; type: TrackType; }>, internalNodes: InternalNode | undefined, @@ -26,4 +29,13 @@ export class PlacedPiece { 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), + ); + } } diff --git a/web/src/pages/game/components/BoardCell.tsx b/web/src/pages/game/components/BoardCell.tsx index 2e1d5af..220e7f6 100644 --- a/web/src/pages/game/components/BoardCell.tsx +++ b/web/src/pages/game/components/BoardCell.tsx @@ -25,7 +25,12 @@ const BoardCell = (props: BoardCellProps) => { const handleBoardCellClick = () => { const selectedDie = dice.find((die) => die.isSelected); if (!selectedDie) return; - cell.placePiece(selectedDie.pieceId); + try { + cell.placePiece(selectedDie.pieceId); + } catch (error) { + console.log(error); + return; + } setDice( dice.map((die) => { return die !== selectedDie From 035b7da335ca4d7cc76b579c87d05ddfa5da1b12 Mon Sep 17 00:00:00 2001 From: MiguelMLorente Date: Tue, 3 Dec 2024 18:09:04 +0100 Subject: [PATCH 3/4] Disallow piece placement when piece is not connected to adjacent piece or exit --- interface/constants/Pieces.ts | 2 +- interface/types/Cell.ts | 101 ++++++++++++++++++++++------------ 2 files changed, 66 insertions(+), 37 deletions(-) diff --git a/interface/constants/Pieces.ts b/interface/constants/Pieces.ts index fc12356..e71f635 100644 --- a/interface/constants/Pieces.ts +++ b/interface/constants/Pieces.ts @@ -294,7 +294,7 @@ const T_JUNCTION_ROAD: Piece = new Piece({ }, { startPoint: Direction.WEST, - type: TrackType.RAIL, + type: TrackType.ROAD, }, ], }); diff --git a/interface/types/Cell.ts b/interface/types/Cell.ts index 9f79fad..9bac0ba 100644 --- a/interface/types/Cell.ts +++ b/interface/types/Cell.ts @@ -1,10 +1,12 @@ import { CellType } from "../constants/CellType"; -import { Direction } from "../constants/Direction"; +import { Direction, directions } from "../constants/Direction"; import { ExitType } from "../constants/ExitType"; import { PieceId, pieceMap } from "../constants/Pieces"; +import { TrackType } from "../constants/TrackType"; import { Exit } from "./Exit"; import { ExternalNode } from "./ExternalNode"; import { InternalNode } from "./InternalNode"; +import { Piece } from "./Piece"; import { PlacedPiece } from "./PlacedPiece"; export class Cell { @@ -33,45 +35,72 @@ export class Cell { public placePiece(pieceId: PieceId) { if (this.placedPiece !== undefined) return; - const piece = pieceMap[pieceId]; - const connectionTypes = Array.from(piece.tracks).flatMap((track) => { - const directions = - track.joinedPoints.secondPoint instanceof InternalNode - ? [track.joinedPoints.firstPoint] - : [track.joinedPoints.firstPoint, track.joinedPoints.secondPoint]; - return directions.map((direction) => { - return { direction: direction, type: track.type }; - }); - }); - connectionTypes.forEach(({ direction, type }) => { - const adjacentPoint = this.getNodeAt(direction).traverseBorder(); - if (adjacentPoint instanceof Exit) { - if ( - adjacentPoint.type !== ExitType.AMBIVALENT && - adjacentPoint.type.toString() !== type.toString() - ) { - throw Error( - `Unable to place piece, invalid border at direction ${direction}`, - ); - } - } else { - if (adjacentPoint.cell.placedPiece !== undefined) { - const adjacentTrack = - adjacentPoint.cell.placedPiece.piece.findTrackForDirection( - adjacentPoint.direction, - ); - if (adjacentTrack !== undefined && adjacentTrack.type !== type) { - throw Error( - `Unable to place piece next to another due to conflicting track types at ${direction}`, - ); - } - } - } - }); + const piece: Piece = pieceMap[pieceId]; + + 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, + }; + }) + .some(({ 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 isTrackConnected; + }); + + if (!hasAnyConnection) { + throw Error("No adjacent exit or piece available to connect to"); + } + } } From 5fc02aa6a6fc62f59e0a3cd0a3f073801215e7ae Mon Sep 17 00:00:00 2001 From: MiguelMLorente Date: Tue, 3 Dec 2024 23:03:28 +0100 Subject: [PATCH 4/4] Enable pieces rotation in dice set and board --- interface/constants/Direction.ts | 17 ++++++++ interface/constants/Pieces.ts | 6 +-- interface/types/Cell.ts | 10 +++-- interface/types/Piece.ts | 29 +++++++++++++- web/public/rotate-icon.png | Bin 0 -> 14454 bytes web/src/pages/game/GamePage.tsx | 1 + web/src/pages/game/components/BoardCell.scss | 12 ++++++ web/src/pages/game/components/BoardCell.tsx | 17 ++++++-- web/src/pages/game/components/DiceSet.scss | 22 +++++++++++ web/src/pages/game/components/DiceSet.tsx | 39 ++++++++++++++++--- web/src/pages/game/components/Die.scss | 14 +++++++ web/src/pages/game/components/Die.tsx | 20 +++++++--- web/src/pages/game/components/GameBoard.tsx | 2 + 13 files changed, 166 insertions(+), 23 deletions(-) create mode 100644 web/public/rotate-icon.png diff --git a/interface/constants/Direction.ts b/interface/constants/Direction.ts index 6bc4347..d44deda 100644 --- a/interface/constants/Direction.ts +++ b/interface/constants/Direction.ts @@ -11,3 +11,20 @@ export const directions: Direction[] = [ Direction.EAST, Direction.WEST, ]; + +export function rotateDirection( + initialDirection: Direction, + rotationAngle: 0 | 90 | 180 | 270, +): Direction { + const angleToDirectionMap: Record = { + [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, + )!; +} diff --git a/interface/constants/Pieces.ts b/interface/constants/Pieces.ts index e71f635..e4bddc3 100644 --- a/interface/constants/Pieces.ts +++ b/interface/constants/Pieces.ts @@ -13,7 +13,6 @@ const STRAIGHT_RAIL: Piece = new Piece({ }, ], }); - const TURN_RAIL: Piece = new Piece({ useInternalTracks: false, trackDefinitions: [ @@ -166,7 +165,7 @@ const DEAD_END_STATION_ROAD: Piece = new Piece({ trackDefinitions: [ { startPoint: Direction.EAST, - type: TrackType.RAIL, + type: TrackType.ROAD, }, ], }); @@ -232,7 +231,6 @@ const FOUR_WAY_WITH_ONE_RAIL: Piece = new Piece({ }, ], }); - const FOUR_WAY_TURNING_CROSSING: Piece = new Piece({ useInternalTracks: true, internalNodeType: InternalNodeType.STATION, @@ -316,7 +314,7 @@ const FOUR_WAY_CROSS_ROAD: Piece = new Piece({ }, { startPoint: Direction.WEST, - type: TrackType.RAIL, + type: TrackType.ROAD, }, ], }); diff --git a/interface/types/Cell.ts b/interface/types/Cell.ts index 9bac0ba..6412cc5 100644 --- a/interface/types/Cell.ts +++ b/interface/types/Cell.ts @@ -1,8 +1,7 @@ import { CellType } from "../constants/CellType"; -import { Direction, directions } from "../constants/Direction"; +import { Direction } from "../constants/Direction"; import { ExitType } from "../constants/ExitType"; import { PieceId, pieceMap } from "../constants/Pieces"; -import { TrackType } from "../constants/TrackType"; import { Exit } from "./Exit"; import { ExternalNode } from "./ExternalNode"; import { InternalNode } from "./InternalNode"; @@ -33,9 +32,12 @@ export class Cell { return node!; } - public placePiece(pieceId: PieceId) { + /** + * @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]; + const piece: Piece = pieceMap[pieceId].rotate(rotation); this.validatePiecePlacement(piece); diff --git a/interface/types/Piece.ts b/interface/types/Piece.ts index f684cf4..953c04d 100644 --- a/interface/types/Piece.ts +++ b/interface/types/Piece.ts @@ -1,4 +1,4 @@ -import { Direction } from "../constants/Direction"; +import { Direction, rotateDirection } from "../constants/Direction"; import { TrackType } from "../constants/TrackType"; import { Cell } from "./Cell"; import { InternalNode } from "./InternalNode"; @@ -65,6 +65,33 @@ export class Piece { } } + 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( diff --git a/web/public/rotate-icon.png b/web/public/rotate-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5af53cacb2d1bb2d8102f5023eb9da43174c7e32 GIT binary patch literal 14454 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 literal 0 HcmV?d00001 diff --git a/web/src/pages/game/GamePage.tsx b/web/src/pages/game/GamePage.tsx index 499bd51..4b06b3d 100644 --- a/web/src/pages/game/GamePage.tsx +++ b/web/src/pages/game/GamePage.tsx @@ -16,6 +16,7 @@ const GamePage = () => { pieceId: getRandomPieceId(), isSelected: false, isDisabled: false, + rotation: 0, }; }), ); diff --git a/web/src/pages/game/components/BoardCell.scss b/web/src/pages/game/components/BoardCell.scss index a7822ed..1250a30 100644 --- a/web/src/pages/game/components/BoardCell.scss +++ b/web/src/pages/game/components/BoardCell.scss @@ -15,6 +15,18 @@ &:nth-child(3) { top: -40px; } + + &.rotate-90 { + transform: rotate(-90deg); + } + + &.rotate-180 { + transform: rotate(180deg); + } + + &.rotate-270 { + transform: rotate(90deg); + } } &.house { diff --git a/web/src/pages/game/components/BoardCell.tsx b/web/src/pages/game/components/BoardCell.tsx index 220e7f6..3170438 100644 --- a/web/src/pages/game/components/BoardCell.tsx +++ b/web/src/pages/game/components/BoardCell.tsx @@ -1,5 +1,6 @@ import { Cell, directions, Exit, PieceId } from "interface"; import "./BoardCell.scss"; +import { useState } from "react"; export interface BoardCellProps { cell: Cell; @@ -7,6 +8,7 @@ export interface BoardCellProps { pieceId: PieceId; isSelected: boolean; isDisabled: boolean; + rotation: number; }[]; setDice: React.Dispatch< React.SetStateAction< @@ -14,6 +16,7 @@ export interface BoardCellProps { pieceId: PieceId; isSelected: boolean; isDisabled: boolean; + rotation: number; }[] > >; @@ -22,11 +25,15 @@ export interface BoardCellProps { const BoardCell = (props: BoardCellProps) => { const { cell, dice, setDice, refreshBoardRender } = props; + const [pieceRotationAngle, setPieceRotationAngle] = useState(0); const handleBoardCellClick = () => { const selectedDie = dice.find((die) => die.isSelected); if (!selectedDie) return; try { - cell.placePiece(selectedDie.pieceId); + cell.placePiece( + selectedDie.pieceId, + selectedDie.rotation as 0 | 90 | 180 | 270, + ); } catch (error) { console.log(error); return; @@ -36,12 +43,13 @@ const BoardCell = (props: BoardCellProps) => { return die !== selectedDie ? die : { - pieceId: selectedDie.pieceId, + ...selectedDie, isSelected: false, isDisabled: true, }; }), ); + setPieceRotationAngle(selectedDie.rotation); refreshBoardRender(); }; @@ -64,7 +72,10 @@ const BoardCell = (props: BoardCellProps) => { ); })} {cell.placedPiece && ( - + )}
); diff --git a/web/src/pages/game/components/DiceSet.scss b/web/src/pages/game/components/DiceSet.scss index 1152b08..87776c6 100644 --- a/web/src/pages/game/components/DiceSet.scss +++ b/web/src/pages/game/components/DiceSet.scss @@ -1,3 +1,25 @@ +.dice-set-actions { + display: flex; + flex-direction: row; + padding: 50px 50px 0; + width: max-content; + margin: auto; + + img { + border: 3px solid green; + box-shadow: 0 0 10px green; + border-radius: 20%; + padding: 5px; + width: 80px; + height: 80px; + margin-right: 10px; + + &.icon-inverted { + transform: scaleX(-1); + } + } +} + .dice-set { display: grid; grid-template-columns: repeat(2, auto); diff --git a/web/src/pages/game/components/DiceSet.tsx b/web/src/pages/game/components/DiceSet.tsx index 8e10fe0..1779190 100644 --- a/web/src/pages/game/components/DiceSet.tsx +++ b/web/src/pages/game/components/DiceSet.tsx @@ -1,12 +1,14 @@ import { PieceId } from "interface"; import "./DiceSet.scss"; import Die from "./Die"; +import React from "react"; export interface DiceSetProps { dice: { pieceId: PieceId; isSelected: boolean; isDisabled: boolean; + rotation: number; }[]; setDice: React.Dispatch< React.SetStateAction< @@ -14,6 +16,7 @@ export interface DiceSetProps { pieceId: PieceId; isSelected: boolean; isDisabled: boolean; + rotation: number; }[] > >; @@ -24,6 +27,7 @@ const DiceSet = (props: DiceSetProps) => { pieceId: PieceId; isSelected: boolean; isDisabled: boolean; + rotation: number; }) => { if (die.isDisabled) return; const newDiceState = dice.map((oldDie) => { @@ -36,12 +40,37 @@ const DiceSet = (props: DiceSetProps) => { setDice(newDiceState); }; + const handleRotateButton = (rotation: number) => { + const newDiceState = dice.map((die) => { + if (!die.isSelected) return die; + const rotationAngle = (die.rotation + rotation + 360) % 360; + return { + ...die, + rotation: rotationAngle, + }; + }); + setDice(newDiceState); + }; + return ( -
- {dice.map((die) => ( - - ))} -
+ +
+ handleRotateButton(-90)} + > + handleRotateButton(90)} + > +
+
+ {dice.map((die) => ( + + ))} +
+
); }; diff --git a/web/src/pages/game/components/Die.scss b/web/src/pages/game/components/Die.scss index d96f7f8..b3f4195 100644 --- a/web/src/pages/game/components/Die.scss +++ b/web/src/pages/game/components/Die.scss @@ -15,4 +15,18 @@ &.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); } \ No newline at end of file diff --git a/web/src/pages/game/components/Die.tsx b/web/src/pages/game/components/Die.tsx index f6644b0..ba8474f 100644 --- a/web/src/pages/game/components/Die.tsx +++ b/web/src/pages/game/components/Die.tsx @@ -2,23 +2,31 @@ import { PieceId } from "interface"; import "./Die.scss"; interface DieProps { - die: { pieceId: PieceId; isSelected: boolean; isDisabled: boolean }; + die: { + pieceId: PieceId; + isSelected: boolean; + isDisabled: boolean; + rotation: number; + }; handleDieClick: (die: { pieceId: PieceId; isSelected: boolean; isDisabled: boolean; + rotation: number; }) => void; } const Die = (props: DieProps) => { const { die, handleDieClick } = props; - const { pieceId, isSelected, isDisabled } = die; + const { pieceId, isSelected, isDisabled, rotation } = die; + const className = + "dice" + + (isSelected ? " selected" : "") + + (isDisabled ? " disabled" : "") + + ` rotate-${rotation}`; return ( -
handleDieClick(die)} - > +
handleDieClick(die)}>
); diff --git a/web/src/pages/game/components/GameBoard.tsx b/web/src/pages/game/components/GameBoard.tsx index a713e7d..0997041 100644 --- a/web/src/pages/game/components/GameBoard.tsx +++ b/web/src/pages/game/components/GameBoard.tsx @@ -8,6 +8,7 @@ export interface GameBoardProps { pieceId: PieceId; isSelected: boolean; isDisabled: boolean; + rotation: number; }[]; setDice: React.Dispatch< React.SetStateAction< @@ -15,6 +16,7 @@ export interface GameBoardProps { pieceId: PieceId; isSelected: boolean; isDisabled: boolean; + rotation: number; }[] > >;