Skip to content

Commit

Permalink
refactor orientaiton state (#58)
Browse files Browse the repository at this point in the history
* rename Orientation to SurfaceOrientationType
* change types
* orientation state variable
* Orientation type
* orientation reducer
* Rename surface orientation to Orientability
* fix instance of checking pentomino object equality
  • Loading branch information
RheingoldRiver authored Oct 28, 2023
1 parent ade2b3f commit 3431567
Show file tree
Hide file tree
Showing 14 changed files with 264 additions and 211 deletions.
30 changes: 15 additions & 15 deletions src/components/Board/Board.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
ChevronRightIcon,
ChevronUpIcon,
} from "@heroicons/react/24/outline";
import { Orientation, SURFACES } from "../../constants";
import { OrientabilityType, SURFACES } from "../../constants";
import { ChevronDoubleUpIcon } from "@heroicons/react/20/solid";

export const Board = forwardRef(({ gridArea }: { gridArea: string }, ref) => {
Expand All @@ -32,31 +32,31 @@ export const Board = forwardRef(({ gridArea }: { gridArea: string }, ref) => {
}}
>
<div className="flex flex-row justify-center items-center h-8" style={{ gridArea: "topOrientation" }}>
{surface.orientation.w !== Orientation.None ? <ChevronRightIcon width={20} /> : ""}
{surface.orientability.w !== OrientabilityType.None ? <ChevronRightIcon width={20} /> : ""}
</div>
<div className="flex flex-col justify-center items-center w-8" style={{ gridArea: "leftOrientation" }}>
{surface.orientation.h !== Orientation.None ? <ChevronDoubleUpIcon width={20} /> : ""}
{surface.orientability.h !== OrientabilityType.None ? <ChevronDoubleUpIcon width={20} /> : ""}
</div>
<div className="flex flex-col justify-center items-center w-8" style={{ gridArea: "rightOrientation" }}>
{
{
[Orientation.None]: "",
[Orientation.Orientable]: <ChevronDoubleUpIcon width={20} />,
[Orientation.Nonorientable]: <ChevronDoubleDownIcon width={20} />,
[Orientation.ConsecutiveNonorientable]: <ChevronDownIcon width={20} />,
[Orientation.ConsecutiveOrientable]: <ChevronUpIcon width={20} />,
}[surface.orientation.h]
[OrientabilityType.None]: "",
[OrientabilityType.Orientable]: <ChevronDoubleUpIcon width={20} />,
[OrientabilityType.Nonorientable]: <ChevronDoubleDownIcon width={20} />,
[OrientabilityType.ConsecutiveNonorientable]: <ChevronDownIcon width={20} />,
[OrientabilityType.ConsecutiveOrientable]: <ChevronUpIcon width={20} />,
}[surface.orientability.h]
}
</div>
<div className="flex flex-row justify-center items-center h-8" style={{ gridArea: "botOrientation" }}>
{
{
[Orientation.None]: "",
[Orientation.Orientable]: <ChevronRightIcon width={20} />,
[Orientation.Nonorientable]: <ChevronLeftIcon width={20} />,
[Orientation.ConsecutiveNonorientable]: <ChevronDoubleLeftIcon width={20} />,
[Orientation.ConsecutiveOrientable]: <ChevronDoubleRightIcon width={20} />,
}[surface.orientation.w]
[OrientabilityType.None]: "",
[OrientabilityType.Orientable]: <ChevronRightIcon width={20} />,
[OrientabilityType.Nonorientable]: <ChevronLeftIcon width={20} />,
[OrientabilityType.ConsecutiveNonorientable]: <ChevronDoubleLeftIcon width={20} />,
[OrientabilityType.ConsecutiveOrientable]: <ChevronDoubleRightIcon width={20} />,
}[surface.orientability.w]
}
</div>
<Grid
Expand Down
146 changes: 65 additions & 81 deletions src/components/GameStateProvider/GameStateProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
import { cloneDeep, debounce } from "lodash";
import { createContext, ReactNode, useState, Dispatch, SetStateAction, useRef, useEffect, useMemo } from "react";
import {
createContext,
ReactNode,
useState,
Dispatch,
SetStateAction,
useRef,
useEffect,
useMemo,
Reducer,
useReducer,
} from "react";
import {
ALL_PENTOMINO_NAMES,
Action,
Colors,
DEFAULT_CONFIG,
Orientation,
PENTOMINO_NAMES,
PaintedCell,
PlacedPentomino,
Expand All @@ -18,19 +30,22 @@ import { deserializeUrl, serializeUrl } from "./urlConfig";
import { useNavigate, useParams } from "react-router-dom";
import useHotkey from "../../hooks/use-hotkey";
import { getPaintedBoard } from "./paintGrid";
import {
OrientationAction,
OrientationActionType,
ReflectionDirection,
RotationDirection,
orientationReducer,
} from "./currentPentominoReducer";

interface GameState {
grid: PlacedPentomino[][];
setGrid: Dispatch<SetStateAction<PlacedPentomino[][]>>;
paintedGrid: PaintedCell[][];
currentPentomino: Pentomino;
currentGridCoords: Coordinates;
currentReflection: number;
currentRotation: number;
rotateLeft: () => void;
rotateRight: () => void;
reflectX: () => void;
reflectY: () => void;
currentOrientation: Orientation;
orientationDispatch: Dispatch<OrientationAction>;
updateCurrentPentomino: (p: Pentomino) => void;
clickBoard: (x: number, y: number) => void;
hoverBoard: (x: number, y: number) => void;
Expand All @@ -54,12 +69,8 @@ const DEFAULT_GAME_STATE: GameState = {
paintedGrid: [],
currentPentomino: PENTOMINOES.None,
currentGridCoords: { x: 0, y: 0 },
currentReflection: 0,
currentRotation: 0,
rotateLeft: () => {},
rotateRight: () => {},
reflectX: () => {},
reflectY: () => {},
currentOrientation: { reflection: 0, rotation: 0 },
orientationDispatch: () => {},
updateCurrentPentomino: () => {},
clickBoard: () => {},
hoverBoard: () => {},
Expand Down Expand Up @@ -109,8 +120,10 @@ export default function GameStateProvider({ children }: { children: ReactNode })

const [currentPentomino, setCurrentPentomino] = useState<Pentomino>(PENTOMINOES.None);
const [currentGridCoords, setCurrentGridCoords] = useState<Coordinates>({ x: -1, y: -1 });
const [currentReflection, setCurrentReflection] = useState<number>(0); // 0, 1
const [currentRotation, setCurrentRotation] = useState<number>(0); // 0, 1, 2, 3
const [currentOrientation, orientationDispatch] = useReducer<Reducer<Orientation, OrientationAction>>(
orientationReducer,
{ reflection: 0, rotation: 0 }
);

const [actionHistory, setActionHistory] = useState<Action[]>([]);
const navigate = useNavigate();
Expand Down Expand Up @@ -148,45 +161,16 @@ export default function GameStateProvider({ children }: { children: ReactNode })
surface,
{
pentomino: currentPentomino,
x: currentGridCoords.x,
y: currentGridCoords.y,
rotation: currentRotation,
reflection: currentReflection,
orientation: { ...currentOrientation },
coordinates: { ...currentGridCoords },
},
boardHovered
);
}, [grid, surface, currentGridCoords, boardHovered, currentPentomino, currentRotation, currentReflection]);

function rotateLeft() {
setCurrentRotation((4 + currentRotation - 1) % 4);
}

function rotateRight() {
setCurrentRotation((currentRotation + 1) % 4);
}

function reflectX() {
if (currentRotation % 2 === 1) {
setCurrentRotation((currentRotation + 2) % 4);
}
setCurrentReflection((currentReflection + 1) % 2);
}

function reflectY() {
if (currentRotation % 2 === 0) {
setCurrentRotation((currentRotation + 2) % 4);
}
setCurrentReflection((currentReflection + 1) % 2);
}

function resetOrientation() {
setCurrentReflection(0);
setCurrentRotation(0);
}
}, [grid, surface, currentGridCoords, boardHovered, currentPentomino, currentOrientation]);

function updateCurrentPentomino(p: Pentomino) {
setCurrentPentomino(p);
resetOrientation();
orientationDispatch({ type: OrientationActionType.replace });
}

function updateGridCoords(dim: keyof Coordinates, dir: number) {
Expand All @@ -208,10 +192,8 @@ export default function GameStateProvider({ children }: { children: ReactNode })
{
// grid not nextGrid
prevName: grid[x][y].pentomino.name,
prevRotation: grid[x][y].rotation,
prevReflection: grid[x][y].reflection,
x: x,
y: y,
prevOrientation: { ...grid[x][y].orientation },
prevCoordinates: { x, y },
},
],
},
Expand All @@ -225,10 +207,8 @@ export default function GameStateProvider({ children }: { children: ReactNode })
const newGrid = [...grid];
newGrid[newX][newY] = {
pentomino: currentPentomino,
reflection: currentReflection,
rotation: currentRotation,
x: newX,
y: newY,
orientation: { ...currentOrientation },
coordinates: { x: newX, y: newY },
};
setGrid(newGrid);
}
Expand All @@ -239,10 +219,11 @@ export default function GameStateProvider({ children }: { children: ReactNode })
if (x === givenX && y === givenY) {
return {
pentomino: PENTOMINOES.None,
reflection: 0,
rotation: 0,
x: x,
y: y,
orientation: {
reflection: 0,
rotation: 0,
},
coordinates: { x, y },
};
}
return c;
Expand All @@ -260,10 +241,8 @@ export default function GameStateProvider({ children }: { children: ReactNode })
) {
nextAction.pentominoes.push({
prevName: c.pentomino.name,
prevReflection: c.reflection,
prevRotation: c.rotation,
x,
y,
prevOrientation: { ...c.orientation },
prevCoordinates: { x, y },
});
}
return {
Expand All @@ -283,9 +262,11 @@ export default function GameStateProvider({ children }: { children: ReactNode })
drawPentomino(x, y);
} else {
setCurrentPentomino(cell.pentomino.pentomino);
erasePentomino(cell.pentomino.x, cell.pentomino.y);
setCurrentRotation(cell.pentomino.rotation);
setCurrentReflection(cell.pentomino.reflection);
erasePentomino(cell.pentomino.coordinates.x, cell.pentomino.coordinates.y);
orientationDispatch({
type: OrientationActionType.replace,
newOrientation: cell.pentomino.orientation,
});
}
}

Expand All @@ -309,9 +290,8 @@ export default function GameStateProvider({ children }: { children: ReactNode })
if (lastAction === undefined) return;
const nextGrid = cloneDeep(grid);
lastAction.pentominoes.forEach((p) => {
nextGrid[p.x][p.y].pentomino = PENTOMINOES[p.prevName];
nextGrid[p.x][p.y].rotation = p.prevRotation;
nextGrid[p.x][p.y].reflection = p.prevReflection;
nextGrid[p.prevCoordinates.x][p.prevCoordinates.y].pentomino = PENTOMINOES[p.prevName];
nextGrid[p.prevCoordinates.x][p.prevCoordinates.y].orientation = { ...p.prevOrientation };
});
setGrid(nextGrid);
setActionHistory(nextActionHistory);
Expand All @@ -337,10 +317,18 @@ export default function GameStateProvider({ children }: { children: ReactNode })
updateGridCoords("x", 1);
});

useHotkey(undefined, "A", rotateLeft);
useHotkey(undefined, "D", rotateRight);
useHotkey(undefined, "S", reflectX);
useHotkey(undefined, "W", reflectY);
useHotkey(undefined, "A", () => {
orientationDispatch({ type: OrientationActionType.rotate, direction: RotationDirection.Left });
});
useHotkey(undefined, "D", () => {
orientationDispatch({ type: OrientationActionType.rotate, direction: RotationDirection.Right });
});
useHotkey(undefined, "S", () => {
orientationDispatch({ type: OrientationActionType.reflect, direction: ReflectionDirection.X });
});
useHotkey(undefined, "W", () => {
orientationDispatch({ type: OrientationActionType.reflect, direction: ReflectionDirection.Y });
});

function updateToolbarPentomino(increment: number) {
const curIndex = ALL_PENTOMINO_NAMES.indexOf(currentPentomino.name);
Expand All @@ -351,7 +339,7 @@ export default function GameStateProvider({ children }: { children: ReactNode })
ALL_PENTOMINO_NAMES[(curIndex + increment + ALL_PENTOMINO_NAMES.length) % ALL_PENTOMINO_NAMES.length]
];
setCurrentPentomino(nextPentomino);
resetOrientation();
orientationDispatch({ type: OrientationActionType.replace });
}

useHotkey(undefined, "E", () => {
Expand All @@ -376,12 +364,8 @@ export default function GameStateProvider({ children }: { children: ReactNode })
paintedGrid,
currentPentomino,
currentGridCoords,
currentReflection,
currentRotation,
rotateLeft,
rotateRight,
reflectX,
reflectY,
currentOrientation,
orientationDispatch,
updateCurrentPentomino,
clickBoard,
hoverBoard,
Expand Down
2 changes: 1 addition & 1 deletion src/components/GameStateProvider/color.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ test("we have the right half num colors", () => {
});

test("pentomino colors are encoded properly", () => {
expect(encodeOrientation(0, 0, 6)).toBe("0");
expect(encodeOrientation({ rotation: 0, reflection: 0 }, 6)).toBe("0");
});

test("a color is encoded properly", () => {
Expand Down
45 changes: 45 additions & 0 deletions src/components/GameStateProvider/currentPentominoReducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { produce } from "immer";

Check failure on line 1 in src/components/GameStateProvider/currentPentominoReducer.ts

View workflow job for this annotation

GitHub Actions / deploy

Cannot find module 'immer' or its corresponding type declarations.
import { Orientation } from "../../constants";
import { Reducer } from "react";

export enum RotationDirection {
Left,
Right,
}

export enum ReflectionDirection {
X,
Y,
}

export enum OrientationActionType {
rotate = "ROTATE",
reflect = "REFLECT",
replace = "REPLACE",
}

export interface OrientationAction {
type: OrientationActionType;
direction?: RotationDirection | ReflectionDirection;
newOrientation?: Orientation;
}

export const orientationReducer: Reducer<Orientation, OrientationAction> = (
currentOrientation: Orientation,
action: OrientationAction
) => {
switch (action.type) {
case OrientationActionType.rotate:
return produce(currentOrientation, (o: Orientation) => {
const direction = action.direction === RotationDirection.Left ? -1 : 1;
o.rotation = (4 + o.rotation + direction) % 4;
});
case OrientationActionType.reflect:
return produce(currentOrientation, (o: Orientation) => {
o.reflection = (o.reflection + 1) % 2;
if (o.rotation % 2 === 1) o.rotation = (o.rotation + 2) % 4;
});
case OrientationActionType.replace:
return action.newOrientation ? { ...action.newOrientation } : { rotation: 0, reflection: 0 };
}
};
Loading

0 comments on commit 3431567

Please sign in to comment.