Compare commits
No commits in common. "3af9caff9a020a78d418cf4b9e435731911a6556" and "e7beb85302e7157314fa3db74787b43c2eddcf1a" have entirely different histories.
3af9caff9a
...
e7beb85302
|
|
@ -11,20 +11,3 @@ export const directions: Direction[] = [
|
||||||
Direction.EAST,
|
Direction.EAST,
|
||||||
Direction.WEST,
|
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,
|
|
||||||
)!;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ const STRAIGHT_RAIL: Piece = new Piece({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const TURN_RAIL: Piece = new Piece({
|
const TURN_RAIL: Piece = new Piece({
|
||||||
useInternalTracks: false,
|
useInternalTracks: false,
|
||||||
trackDefinitions: [
|
trackDefinitions: [
|
||||||
|
|
@ -165,7 +166,7 @@ const DEAD_END_STATION_ROAD: Piece = new Piece({
|
||||||
trackDefinitions: [
|
trackDefinitions: [
|
||||||
{
|
{
|
||||||
startPoint: Direction.EAST,
|
startPoint: Direction.EAST,
|
||||||
type: TrackType.ROAD,
|
type: TrackType.RAIL,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
@ -231,6 +232,7 @@ const FOUR_WAY_WITH_ONE_RAIL: Piece = new Piece({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const FOUR_WAY_TURNING_CROSSING: Piece = new Piece({
|
const FOUR_WAY_TURNING_CROSSING: Piece = new Piece({
|
||||||
useInternalTracks: true,
|
useInternalTracks: true,
|
||||||
internalNodeType: InternalNodeType.STATION,
|
internalNodeType: InternalNodeType.STATION,
|
||||||
|
|
@ -292,7 +294,7 @@ const T_JUNCTION_ROAD: Piece = new Piece({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
startPoint: Direction.WEST,
|
startPoint: Direction.WEST,
|
||||||
type: TrackType.ROAD,
|
type: TrackType.RAIL,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
@ -314,7 +316,7 @@ const FOUR_WAY_CROSS_ROAD: Piece = new Piece({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
startPoint: Direction.WEST,
|
startPoint: Direction.WEST,
|
||||||
type: TrackType.ROAD,
|
type: TrackType.RAIL,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,5 @@ export * from "./types/ExternalNode";
|
||||||
export * from "./types/InternalNode";
|
export * from "./types/InternalNode";
|
||||||
export * from "./types/Piece";
|
export * from "./types/Piece";
|
||||||
export * from "./types/PlacedPiece";
|
export * from "./types/PlacedPiece";
|
||||||
|
export * from "./types/Node";
|
||||||
export * from "./BoardBuilder";
|
export * from "./BoardBuilder";
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,10 @@
|
||||||
import { CellType } from "../constants/CellType";
|
import { CellType } from "../constants/CellType";
|
||||||
import { Direction } from "../constants/Direction";
|
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 { ExternalNode } from "./ExternalNode";
|
||||||
import { InternalNode } from "./InternalNode";
|
|
||||||
import { Piece } from "./Piece";
|
|
||||||
import { PlacedPiece } from "./PlacedPiece";
|
|
||||||
|
|
||||||
export class Cell {
|
export class Cell {
|
||||||
public readonly externalNodes: Map<Direction, ExternalNode>;
|
public readonly externalNodes: Map<Direction, ExternalNode>;
|
||||||
public readonly cellType: CellType;
|
public readonly cellType: CellType;
|
||||||
public placedPiece?: {
|
|
||||||
piece: PlacedPiece;
|
|
||||||
id: PieceId;
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(cellType: CellType) {
|
constructor(cellType: CellType) {
|
||||||
this.externalNodes = new Map([
|
this.externalNodes = new Map([
|
||||||
|
|
@ -31,78 +21,4 @@ export class Cell {
|
||||||
if (!node) throw Error(`Could not find node at ${direction}`);
|
if (!node) throw Error(`Could not find node at ${direction}`);
|
||||||
return node!;
|
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,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { ExternalNode } from "./ExternalNode";
|
||||||
|
import { InternalNode } from "./InternalNode";
|
||||||
|
|
||||||
|
export type Node = ExternalNode | InternalNode;
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { Direction, rotateDirection } from "../constants/Direction";
|
import { Direction } from "../constants/Direction";
|
||||||
import { TrackType } from "../constants/TrackType";
|
import { TrackType } from "../constants/TrackType";
|
||||||
import { Cell } from "./Cell";
|
import { Cell } from "./Cell";
|
||||||
import { InternalNode } from "./InternalNode";
|
import { InternalNode } from "./InternalNode";
|
||||||
import { PlacedPiece } from "./PlacedPiece";
|
import { PlacedPiece } from "./PlacedPiece";
|
||||||
|
import { Node } from "./Node";
|
||||||
import { InternalNodeType } from "../constants/InternalNodeType";
|
import { InternalNodeType } from "../constants/InternalNodeType";
|
||||||
|
|
||||||
export interface PieceProps {
|
export interface PieceProps {
|
||||||
|
|
@ -65,33 +66,6 @@ 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) {
|
toPlacedPiece(cell: Cell) {
|
||||||
return new PlacedPiece(
|
return new PlacedPiece(
|
||||||
new Set(
|
new Set(
|
||||||
|
|
@ -100,8 +74,8 @@ export class Piece {
|
||||||
nodes: {
|
nodes: {
|
||||||
firstNode: cell.getNodeAt(track.joinedPoints.firstPoint),
|
firstNode: cell.getNodeAt(track.joinedPoints.firstPoint),
|
||||||
secondNode:
|
secondNode:
|
||||||
track.joinedPoints.secondPoint instanceof InternalNode
|
track.joinedPoints.secondPoint instanceof Node
|
||||||
? (track.joinedPoints.secondPoint as InternalNode)
|
? (track.joinedPoints.secondPoint as Node)
|
||||||
: cell.getNodeAt(track.joinedPoints.secondPoint as Direction),
|
: cell.getNodeAt(track.joinedPoints.secondPoint as Direction),
|
||||||
},
|
},
|
||||||
type: track.type,
|
type: track.type,
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,22 @@
|
||||||
import { Direction } from "../constants/Direction";
|
|
||||||
import { TrackType } from "../constants/TrackType";
|
import { TrackType } from "../constants/TrackType";
|
||||||
import { Cell } from "./Cell";
|
import { Cell } from "./Cell";
|
||||||
import { ExternalNode } from "./ExternalNode";
|
|
||||||
import { InternalNode } from "./InternalNode";
|
import { InternalNode } from "./InternalNode";
|
||||||
|
import { Node } from "./Node";
|
||||||
|
|
||||||
export class PlacedPiece {
|
export class PlacedPiece {
|
||||||
tracks: Set<{
|
tracks: Set<{
|
||||||
nodes: {
|
nodes: {
|
||||||
firstNode: ExternalNode;
|
firstNode: Node;
|
||||||
secondNode: ExternalNode | InternalNode;
|
secondNode: Node;
|
||||||
};
|
};
|
||||||
type: TrackType;
|
type: TrackType;
|
||||||
}>;
|
}>;
|
||||||
internalNode?: InternalNode;
|
internalNode?: InternalNode;
|
||||||
cell: Cell;
|
cell: Cell;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
tracks: Set<{
|
tracks: Set<{
|
||||||
nodes: {
|
nodes: { firstNode: Node; secondNode: Node };
|
||||||
firstNode: ExternalNode;
|
|
||||||
secondNode: ExternalNode | InternalNode;
|
|
||||||
};
|
|
||||||
type: TrackType;
|
type: TrackType;
|
||||||
}>,
|
}>,
|
||||||
internalNodes: InternalNode | undefined,
|
internalNodes: InternalNode | undefined,
|
||||||
|
|
@ -29,13 +26,4 @@ export class PlacedPiece {
|
||||||
this.internalNode = internalNodes;
|
this.internalNode = internalNodes;
|
||||||
this.cell = cell;
|
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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
|
|
@ -1,33 +1,16 @@
|
||||||
import React, { useState } from "react";
|
import React from "react";
|
||||||
import GameBoard from "./components/GameBoard";
|
import GameBoard from "./components/GameBoard";
|
||||||
import DiceSet from "./components/DiceSet";
|
import DiceSet from "./components/DiceSet";
|
||||||
import "./GamePage.scss";
|
import "./GamePage.scss";
|
||||||
import { PieceId } from "interface";
|
|
||||||
|
|
||||||
const GamePage = () => {
|
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,
|
|
||||||
rotation: 0,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<h1>Game Page Title</h1>
|
<h1>Game Page Title</h1>
|
||||||
<div className="game-panel">
|
<div className="game-panel">
|
||||||
<GameBoard dice={dice} setDice={setDice} />
|
<GameBoard />
|
||||||
<div className="rigth-panel">
|
<div className="rigth-panel">
|
||||||
<DiceSet dice={dice} setDice={setDice} />
|
<DiceSet />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|
|
||||||
|
|
@ -1,141 +0,0 @@
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.rotate-90 {
|
|
||||||
transform: rotate(-90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.rotate-180 {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.rotate-270 {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.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%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
import { Cell, directions, Exit, PieceId } from "interface";
|
|
||||||
import "./BoardCell.scss";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
export interface BoardCellProps {
|
|
||||||
cell: Cell;
|
|
||||||
dice: {
|
|
||||||
pieceId: PieceId;
|
|
||||||
isSelected: boolean;
|
|
||||||
isDisabled: boolean;
|
|
||||||
rotation: number;
|
|
||||||
}[];
|
|
||||||
setDice: React.Dispatch<
|
|
||||||
React.SetStateAction<
|
|
||||||
{
|
|
||||||
pieceId: PieceId;
|
|
||||||
isSelected: boolean;
|
|
||||||
isDisabled: boolean;
|
|
||||||
rotation: number;
|
|
||||||
}[]
|
|
||||||
>
|
|
||||||
>;
|
|
||||||
refreshBoardRender: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
selectedDie.rotation as 0 | 90 | 180 | 270,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setDice(
|
|
||||||
dice.map((die) => {
|
|
||||||
return die !== selectedDie
|
|
||||||
? die
|
|
||||||
: {
|
|
||||||
...selectedDie,
|
|
||||||
isSelected: false,
|
|
||||||
isDisabled: true,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
setPieceRotationAngle(selectedDie.rotation);
|
|
||||||
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 rotate-${pieceRotationAngle}`}
|
|
||||||
src={`pieces/${cell.placedPiece.id}.jpeg`}
|
|
||||||
></img>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BoardCell;
|
|
||||||
|
|
@ -1,28 +1,17 @@
|
||||||
.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 {
|
.dice-set {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, auto);
|
grid-template-columns: repeat(2, auto);
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
padding: 50px;
|
padding: 50px;
|
||||||
|
|
||||||
|
.dice {
|
||||||
|
height: 150px;
|
||||||
|
width: 150px;
|
||||||
|
border: 2px solid black;
|
||||||
|
border-radius: 10%;
|
||||||
|
|
||||||
|
img{
|
||||||
|
border-radius: 10%;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,76 +1,21 @@
|
||||||
import { PieceId } from "interface";
|
import { PieceId } from "interface";
|
||||||
import "./DiceSet.scss";
|
import "./DiceSet.scss";
|
||||||
import Die from "./Die";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export interface DiceSetProps {
|
const DiceSet = () => {
|
||||||
dice: {
|
const getRandomPieceId = () => {
|
||||||
pieceId: PieceId;
|
const randomId = Math.floor(Math.random() * Object.keys(PieceId).length);
|
||||||
isSelected: boolean;
|
return Object.values(PieceId)[randomId];
|
||||||
isDisabled: boolean;
|
|
||||||
rotation: number;
|
|
||||||
}[];
|
|
||||||
setDice: React.Dispatch<
|
|
||||||
React.SetStateAction<
|
|
||||||
{
|
|
||||||
pieceId: PieceId;
|
|
||||||
isSelected: boolean;
|
|
||||||
isDisabled: boolean;
|
|
||||||
rotation: number;
|
|
||||||
}[]
|
|
||||||
>
|
|
||||||
>;
|
|
||||||
}
|
|
||||||
const DiceSet = (props: DiceSetProps) => {
|
|
||||||
const { dice, setDice } = props;
|
|
||||||
const handleDieClick = (die: {
|
|
||||||
pieceId: PieceId;
|
|
||||||
isSelected: boolean;
|
|
||||||
isDisabled: boolean;
|
|
||||||
rotation: number;
|
|
||||||
}) => {
|
|
||||||
if (die.isDisabled) return;
|
|
||||||
const newDiceState = dice.map((oldDie) => {
|
|
||||||
const isSelected = die === oldDie;
|
|
||||||
return {
|
|
||||||
...oldDie,
|
|
||||||
isSelected: isSelected,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
const displayedPieceIds = [1, 2, 3, 4].map(getRandomPieceId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<div className="dice-set">
|
||||||
<div className="dice-set-actions">
|
{displayedPieceIds.map((pieceId) => (
|
||||||
<img
|
<div className="dice">
|
||||||
src="./rotate-icon.png"
|
<img src={`pieces/${pieceId}.jpeg`}></img>
|
||||||
onClick={() => handleRotateButton(-90)}
|
</div>
|
||||||
></img>
|
))}
|
||||||
<img
|
</div>
|
||||||
src="./rotate-icon.png"
|
|
||||||
className="icon-inverted"
|
|
||||||
onClick={() => handleRotateButton(90)}
|
|
||||||
></img>
|
|
||||||
</div>
|
|
||||||
<div className="dice-set">
|
|
||||||
{dice.map((die) => (
|
|
||||||
<Die die={die} handleDieClick={handleDieClick} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
.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);
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import { PieceId } from "interface";
|
|
||||||
import "./Die.scss";
|
|
||||||
|
|
||||||
interface DieProps {
|
|
||||||
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, 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;
|
|
||||||
|
|
@ -7,4 +7,121 @@
|
||||||
grid-template-columns: repeat(7, auto);
|
grid-template-columns: repeat(7, auto);
|
||||||
gap: auto;
|
gap: auto;
|
||||||
padding: 50px;
|
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%);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,44 +1,28 @@
|
||||||
import { buildBoard, PieceId } from "interface";
|
import { buildBoard, Cell, directions, Exit } from "interface";
|
||||||
import "./GameBoard.scss";
|
import "./GameBoard.scss";
|
||||||
import { useState } from "react";
|
|
||||||
import BoardCell from "./BoardCell";
|
|
||||||
|
|
||||||
export interface GameBoardProps {
|
const GameBoard = () => {
|
||||||
dice: {
|
const board: Cell[][] = buildBoard();
|
||||||
pieceId: PieceId;
|
|
||||||
isSelected: boolean;
|
|
||||||
isDisabled: boolean;
|
|
||||||
rotation: number;
|
|
||||||
}[];
|
|
||||||
setDice: React.Dispatch<
|
|
||||||
React.SetStateAction<
|
|
||||||
{
|
|
||||||
pieceId: PieceId;
|
|
||||||
isSelected: boolean;
|
|
||||||
isDisabled: boolean;
|
|
||||||
rotation: number;
|
|
||||||
}[]
|
|
||||||
>
|
|
||||||
>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const GameBoard = (props: GameBoardProps) => {
|
|
||||||
const [board, setBoard] = useState(buildBoard());
|
|
||||||
const [id, setId] = useState(1);
|
|
||||||
const refreshBoardRender = () => {
|
|
||||||
setBoard(board);
|
|
||||||
setId(id + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="game-board" id={id.toString()}>
|
<div className="game-board">
|
||||||
{board.flatMap((row) =>
|
{board.flatMap((row) =>
|
||||||
row.map((cell) => (
|
row.map((cell) => (
|
||||||
<BoardCell
|
<div className={"cell " + cell.cellType.toLowerCase()}>
|
||||||
{...props}
|
{directions.map((direction) => {
|
||||||
cell={cell}
|
const traversedNode = cell.getNodeAt(direction).traverseBorder();
|
||||||
refreshBoardRender={refreshBoardRender}
|
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>
|
||||||
)),
|
)),
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue