Enable placing pieces in the game board

pull/8/head
MiguelMLorente 2024-12-03 12:29:10 +01:00
parent 835ce2c62a
commit 1ab3db8f67
11 changed files with 345 additions and 162 deletions

View File

@ -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<Direction, ExternalNode>;
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,
};
}
}

View File

@ -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,

View File

@ -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 (
<React.Fragment>
<h1>Game Page Title</h1>
<div className="game-panel">
<GameBoard />
<GameBoard dice={dice} setDice={setDice} />
<div className="rigth-panel">
<DiceSet />
<DiceSet dice={dice} setDice={setDice} />
</div>
</div>
</React.Fragment>

View File

@ -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%);
}
}
}

View File

@ -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 (
<div
className={"cell " + cell.cellType.toLowerCase()}
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" src={`pieces/${cell.placedPiece.id}.jpeg`}></img>
)}
</div>
);
};
export default BoardCell;

View File

@ -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%;
}
}
}

View File

@ -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 (
<div className="dice-set">
{displayedPieceIds.map((pieceId) => (
<div className="dice">
<img src={`pieces/${pieceId}.jpeg`}></img>
</div>
{dice.map((die) => (
<Die die={die} handleDieClick={handleDieClick} />
))}
</div>
);

View File

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

View File

@ -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 (
<div
className={`dice${isSelected ? " selected" : ""}${isDisabled ? " disabled" : ""}`}
onClick={() => handleDieClick(die)}
>
<img src={`pieces/${pieceId}.jpeg`}></img>
</div>
);
};
export default Die;

View File

@ -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%);
}
}
}

View File

@ -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 (
<div className="game-board">
<div className="game-board" id={id.toString()}>
{board.flatMap((row) =>
row.map((cell) => (
<div className={"cell " + cell.cellType.toLowerCase()}>
{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>
);
})}
</div>
<BoardCell
{...props}
cell={cell}
refreshBoardRender={refreshBoardRender}
/>
)),
)}
</div>