diff --git a/src/components/Board/Board.tsx b/src/components/Board/Board.tsx index f6f138d..dae7f1d 100644 --- a/src/components/Board/Board.tsx +++ b/src/components/Board/Board.tsx @@ -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) => { @@ -32,31 +32,31 @@ export const Board = forwardRef(({ gridArea }: { gridArea: string }, ref) => { }} >
- {surface.orientation.w !== Orientation.None ? : ""} + {surface.orientability.w !== OrientabilityType.None ? : ""}
- {surface.orientation.h !== Orientation.None ? : ""} + {surface.orientability.h !== OrientabilityType.None ? : ""}
{ { - [Orientation.None]: "", - [Orientation.Orientable]: , - [Orientation.Nonorientable]: , - [Orientation.ConsecutiveNonorientable]: , - [Orientation.ConsecutiveOrientable]: , - }[surface.orientation.h] + [OrientabilityType.None]: "", + [OrientabilityType.Orientable]: , + [OrientabilityType.Nonorientable]: , + [OrientabilityType.ConsecutiveNonorientable]: , + [OrientabilityType.ConsecutiveOrientable]: , + }[surface.orientability.h] }
{ { - [Orientation.None]: "", - [Orientation.Orientable]: , - [Orientation.Nonorientable]: , - [Orientation.ConsecutiveNonorientable]: , - [Orientation.ConsecutiveOrientable]: , - }[surface.orientation.w] + [OrientabilityType.None]: "", + [OrientabilityType.Orientable]: , + [OrientabilityType.Nonorientable]: , + [OrientabilityType.ConsecutiveNonorientable]: , + [OrientabilityType.ConsecutiveOrientable]: , + }[surface.orientability.w] }
void; - rotateRight: () => void; - reflectX: () => void; - reflectY: () => void; + currentOrientation: Orientation; + orientationDispatch: Dispatch; updateCurrentPentomino: (p: Pentomino) => void; clickBoard: (x: number, y: number) => void; hoverBoard: (x: number, y: number) => void; @@ -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: () => {}, @@ -109,8 +120,10 @@ export default function GameStateProvider({ children }: { children: ReactNode }) const [currentPentomino, setCurrentPentomino] = useState(PENTOMINOES.None); const [currentGridCoords, setCurrentGridCoords] = useState({ x: -1, y: -1 }); - const [currentReflection, setCurrentReflection] = useState(0); // 0, 1 - const [currentRotation, setCurrentRotation] = useState(0); // 0, 1, 2, 3 + const [currentOrientation, orientationDispatch] = useReducer>( + orientationReducer, + { reflection: 0, rotation: 0 } + ); const [actionHistory, setActionHistory] = useState([]); const navigate = useNavigate(); @@ -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) { @@ -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 }, }, ], }, @@ -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); } @@ -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; @@ -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 { @@ -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, + }); } } @@ -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); @@ -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); @@ -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", () => { @@ -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, diff --git a/src/components/GameStateProvider/color.test.ts b/src/components/GameStateProvider/color.test.ts index 9184c1e..3cf272d 100644 --- a/src/components/GameStateProvider/color.test.ts +++ b/src/components/GameStateProvider/color.test.ts @@ -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", () => { diff --git a/src/components/GameStateProvider/currentPentominoReducer.ts b/src/components/GameStateProvider/currentPentominoReducer.ts new file mode 100644 index 0000000..ba009f4 --- /dev/null +++ b/src/components/GameStateProvider/currentPentominoReducer.ts @@ -0,0 +1,45 @@ +import { produce } from "immer"; +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 = ( + 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 }; + } +}; diff --git a/src/components/GameStateProvider/orientation.test.ts b/src/components/GameStateProvider/orientation.test.ts index 98575c3..4002681 100644 --- a/src/components/GameStateProvider/orientation.test.ts +++ b/src/components/GameStateProvider/orientation.test.ts @@ -2,64 +2,57 @@ import { expect, test } from "vitest"; import { decodeOrientation, encodeOrientation } from "./urlConfig"; test("encode an orientation with color 0", () => { - expect(encodeOrientation(0, 0, 0)).toBe("0"); - expect(encodeOrientation(1, 0, 0)).toBe("1"); - expect(encodeOrientation(1, 1, 0)).toBe("5"); + expect(encodeOrientation({ rotation: 0, reflection: 0 }, 0)).toBe("0"); + expect(encodeOrientation({ rotation: 1, reflection: 0 }, 0)).toBe("1"); + expect(encodeOrientation({ rotation: 1, reflection: 1 }, 0)).toBe("5"); }); test("encode an orientation in uppercase range", () => { - expect(encodeOrientation(0, 0, 1)).toBe("A"); - expect(encodeOrientation(1, 0, 1)).toBe("B"); - expect(encodeOrientation(0, 0, 2)).toBe("I"); + expect(encodeOrientation({ rotation: 0, reflection: 0 }, 1)).toBe("A"); + expect(encodeOrientation({ rotation: 1, reflection: 0 }, 1)).toBe("B"); + expect(encodeOrientation({ rotation: 0, reflection: 0 }, 2)).toBe("I"); - expect(encodeOrientation(0, 0, 4)).toBe("Y"); - expect(encodeOrientation(1, 0, 4)).toBe("Z"); + expect(encodeOrientation({ rotation: 0, reflection: 0 }, 4)).toBe("Y"); + expect(encodeOrientation({ rotation: 1, reflection: 0 }, 4)).toBe("Z"); }); test("encode an orientation in lowercase range", () => { - expect(encodeOrientation(2, 0, 4)).toBe("a"); + expect(encodeOrientation({ rotation: 2, reflection: 0 }, 4)).toBe("a"); }); test("decode an orientation with numerical color", () => { expect(decodeOrientation("0")).toStrictEqual({ - rotation: 0, - reflection: 0, + orientation: { rotation: 0, reflection: 0 }, color: 0, }); expect(decodeOrientation("5")).toStrictEqual({ - rotation: 1, - reflection: 1, + orientation: { rotation: 1, reflection: 1 }, color: 0, }); expect(decodeOrientation("1")).toStrictEqual({ - rotation: 1, - reflection: 0, + orientation: { rotation: 1, reflection: 0 }, color: 0, }); }); test("decode an orientation with uppercase color", () => { expect(decodeOrientation("A")).toStrictEqual({ - rotation: 0, - reflection: 0, + orientation: { rotation: 0, reflection: 0 }, color: 1, }); expect(decodeOrientation("B")).toStrictEqual({ - rotation: 1, - reflection: 0, + orientation: { rotation: 1, reflection: 0 }, color: 1, }); expect(decodeOrientation("I")).toStrictEqual({ - rotation: 0, - reflection: 0, + orientation: { rotation: 0, reflection: 0 }, color: 2, }); }); test("decode an orientation with lowercase color", () => { expect(decodeOrientation("a")).toStrictEqual({ - rotation: 2, - reflection: 0, + orientation: { rotation: 2, reflection: 0 }, color: 4, }); }); diff --git a/src/components/GameStateProvider/paintGrid.test.ts b/src/components/GameStateProvider/paintGrid.test.ts index ed53dce..804bfcd 100644 --- a/src/components/GameStateProvider/paintGrid.test.ts +++ b/src/components/GameStateProvider/paintGrid.test.ts @@ -52,11 +52,12 @@ test("no incorrect conflicts where 2 pieces touch", () => { paintCell( paintedGrid, { - x: 0, - y: 0, + coordinates: { x: 0, y: 0 }, pentomino: PENTOMINOES.F, - rotation: 0, - reflection: 0, + orientation: { + rotation: 0, + reflection: 0, + }, }, SURFACES.Rectangle, grid, @@ -65,11 +66,12 @@ test("no incorrect conflicts where 2 pieces touch", () => { paintCell( paintedGrid, { - x: 4, - y: 1, + coordinates: { x: 4, y: 1 }, pentomino: PENTOMINOES.V, - rotation: 0, - reflection: 0, + orientation: { + rotation: 0, + reflection: 0, + }, }, SURFACES.Rectangle, grid, @@ -78,17 +80,21 @@ test("no incorrect conflicts where 2 pieces touch", () => { paintCell( paintedGrid, { - x: 2, - y: 1, + coordinates: { x: 2, y: 1 }, pentomino: PENTOMINOES.I, - rotation: 0, - reflection: 0, + orientation: { + rotation: 0, + reflection: 0, + }, }, SURFACES.Rectangle, grid, false ); - expect(paintedGrid[0][1].conflict).toBe(false); + expect(paintedGrid[0][1].conflict).toStrictEqual({ + tileName: "F", + type: ConflictType.Overflow, + }); expect(paintedGrid[0][0].conflict).toStrictEqual({ tileName: "F", type: ConflictType.Overflow, diff --git a/src/components/GameStateProvider/paintGrid.ts b/src/components/GameStateProvider/paintGrid.ts index 12c6ff9..2c64be3 100644 --- a/src/components/GameStateProvider/paintGrid.ts +++ b/src/components/GameStateProvider/paintGrid.ts @@ -2,7 +2,7 @@ import { ConflictType, PlacedPentomino } from "./../../constants"; import { Borders, PaintedCell } from "../../constants"; import { range } from "lodash"; import { PENTOMINOES } from "../../pentominoes"; -import { EMPTY_PENTOMINO, Orientation, SURFACES, Surface } from "../../constants"; +import { EMPTY_PENTOMINO, OrientabilityType, SURFACES, Surface } from "../../constants"; interface NewCoordinates { newX: number; @@ -26,7 +26,8 @@ export function getPaintedBoard( if (currentPlacedPentomino === undefined) return paintedGrid; if ( boardHovered && - paintedGrid[currentPlacedPentomino.x][currentPlacedPentomino.y].pentomino.pentomino.name === PENTOMINOES.None.name + paintedGrid[currentPlacedPentomino.coordinates.x][currentPlacedPentomino.coordinates.y].pentomino.pentomino.name === + PENTOMINOES.None.name ) { paintCell(paintedGrid, currentPlacedPentomino, surface, grid, true); } @@ -59,12 +60,12 @@ export const paintCell = ( grid: PlacedPentomino[][], hovered: boolean ) => { - const orientation = p.pentomino.orientations[p.reflection][p.rotation]; + const orientation = p.pentomino.shapes[p.orientation.reflection][p.orientation.rotation]; orientation.shape.forEach((pr, px) => pr.forEach((val, py) => { if (val === 0) return; // the pentomino isn't taking up this square of its grid, return - const rawX = p.x + px - orientation.center.x; - const rawY = p.y + py - orientation.center.y; + const rawX = p.coordinates.x + px - orientation.center.x; + const rawY = p.coordinates.y + py - orientation.center.y; const height = grid.length; const width = grid[0].length; const { newX, newY } = getCoordinatesToPaint(surface, height, width, rawX, rawY); @@ -80,8 +81,8 @@ export const paintCell = ( cellToPaint.conflict.type = ConflictType.Intersection; } cellToPaint.pentomino = p; - const flipX = outOfBounds(rawY, width) && surface.orientation.h === Orientation.Nonorientable; - const flipY = outOfBounds(rawX, height) && surface.orientation.w === Orientation.Nonorientable; + const flipX = outOfBounds(rawY, width) && surface.orientability.h === OrientabilityType.Nonorientable; + const flipY = outOfBounds(rawX, height) && surface.orientability.w === OrientabilityType.Nonorientable; const transposeX = outOfBounds(rawX, width) && surface.consecutive; const transposeY = outOfBounds(rawY, height) && surface.consecutive; diff --git a/src/components/GameStateProvider/urlConfig.ts b/src/components/GameStateProvider/urlConfig.ts index d776480..26d9ef0 100644 --- a/src/components/GameStateProvider/urlConfig.ts +++ b/src/components/GameStateProvider/urlConfig.ts @@ -3,6 +3,7 @@ import { DEFAULT_COLORS, letterToSurface, MAX_NUM_COLORS, + Orientation, PENTOMINO_NAMES, PlacedPentomino, randomPentominoColors, @@ -39,9 +40,8 @@ export interface StringifiedUrlConfig { surface: Surface; } -interface Orientation { - rotation: number; - reflection: number; +interface OrientationCharacter { + orientation: Orientation; color: number; // index of the color in an array } @@ -85,8 +85,8 @@ export function encodePentomino(p: string, color: number) { return p.toLowerCase(); } -export function encodeOrientation(rotation: number, reflection: number, color: number): string { - const r = reflection === 0 ? rotation : NUM_ROTATIONS + rotation; +export function encodeOrientation(orientation: Orientation, color: number): string { + const r = orientation.reflection === 0 ? orientation.rotation : NUM_ROTATIONS + orientation.rotation; if (color >= HALF_NUM_COLORS) color = color - HALF_NUM_COLORS; if (color === 0 || color === undefined) return r.toString(); @@ -196,7 +196,7 @@ export function serializeUrl({ grid, colors, surface }: UrlConfig): string { } else if (p.pentomino.name !== PENTOMINOES.None.name) placedPentominoes.push({ p: encodePentomino(p.pentomino.name, colors[p.pentomino.name]), - r: encodeOrientation(p.rotation, p.reflection, colors[p.pentomino.name]), + r: encodeOrientation(p.orientation, colors[p.pentomino.name]), c: `${encodeNumber(x)}${encodeNumber(y)}`, }); }) @@ -383,12 +383,14 @@ export function decodeUrl(s: string): StringifiedUrlConfig { return config; } -export function decodeOrientation(r: string): Orientation { +export function decodeOrientation(r: string): OrientationCharacter { const asNumber = toNumber(r); if (!isNaN(asNumber)) { return { - rotation: asNumber % NUM_ROTATIONS, - reflection: asNumber >= NUM_ROTATIONS ? 1 : 0, + orientation: { + rotation: asNumber % NUM_ROTATIONS, + reflection: asNumber >= NUM_ROTATIONS ? 1 : 0, + }, color: 0, }; } else { @@ -399,8 +401,10 @@ export function decodeOrientation(r: string): Orientation { : charCode - UPPERCASE_START_INDEX; const o = charValue % NUM_SPATIAL_ORIENTATIONS; return { - rotation: o % 4, - reflection: o >= 4 ? 1 : 0, + orientation: { + rotation: o % 4, + reflection: o >= 4 ? 1 : 0, + }, // color 0 is a digit not a letter so add 1 color: Math.floor(charValue / NUM_SPATIAL_ORIENTATIONS) + 1, }; @@ -465,10 +469,8 @@ export function deserializeUrl(s: string, defaultRandomColors: boolean): UrlConf const { x, y } = decodeCoordinates(p.c, p.p === p.p.toUpperCase()); ret.grid[x][y] = { pentomino: PENTOMINOES[p.p.toUpperCase()], - rotation: r.rotation, - reflection: r.reflection, - x: x, - y: y, + orientation: { ...r.orientation }, + coordinates: { x, y }, }; if (p.p.toUpperCase() !== PENTOMINOES.R.name) ret.colors[p.p.toUpperCase()] = decodeColor(r.color, p.p, legacy); }); diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 6bc23be..d9e6dd0 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -7,14 +7,8 @@ import { ALL_PENTOMINO_NAMES } from "../../constants"; import { AppStateContext } from "../AppStateProvider/AppStateProvider"; export const Header = ({ ...rest }) => { - const { - currentPentomino, - updateCurrentPentomino, - currentRotation, - currentReflection, - pentominoColors, - showKeyboardIndicators, - } = useContext(GameStateContext); + const { currentPentomino, updateCurrentPentomino, currentOrientation, pentominoColors, showKeyboardIndicators } = + useContext(GameStateContext); const { appPreferences } = useContext(AppStateContext); @@ -62,8 +56,7 @@ export const Header = ({ ...rest }) => { diff --git a/src/components/PentominoDisplay/PentominoDisplay.tsx b/src/components/PentominoDisplay/PentominoDisplay.tsx index 3bc0eb1..acdbe0b 100644 --- a/src/components/PentominoDisplay/PentominoDisplay.tsx +++ b/src/components/PentominoDisplay/PentominoDisplay.tsx @@ -1,5 +1,5 @@ import clsx from "clsx"; -import { PENTOMINO_DIMENSIONS, PENTOMINO_SIZES } from "../../constants"; +import { Orientation, PENTOMINO_DIMENSIONS, PENTOMINO_SIZES } from "../../constants"; import { Coordinates, Pentomino, PENTOMINOES } from "../../pentominoes"; import { useContext } from "react"; import { GameStateContext } from "../GameStateProvider/GameStateProvider"; @@ -9,8 +9,7 @@ import { AppStateContext } from "../AppStateProvider/AppStateProvider"; export const PentominoDisplay = ({ pentomino, color, - rotation = 0, - reflection = 0, + orientation = { rotation: 0, reflection: 0 }, checkGrid = true, size = 5, showCenter = true, @@ -19,8 +18,7 @@ export const PentominoDisplay = ({ }: { pentomino: Pentomino; color?: string; - rotation?: number; - reflection?: number; + orientation?: Orientation; checkGrid?: boolean; size?: number; showCenter?: boolean; @@ -29,9 +27,9 @@ export const PentominoDisplay = ({ const { grid } = useContext(GameStateContext); const { appPreferences } = useContext(AppStateContext); if (color === undefined) color = appPreferences.displayColors[0]; - const p = pentomino.orientations[reflection][rotation]; + const p = pentomino.shapes[orientation.reflection][orientation.rotation]; function bgColor(cell: number, coordinates: Coordinates) { - if (pentomino === PENTOMINOES.R) return { class: "bg-gray-600", style: "" }; + if (pentomino.name === PENTOMINOES.R.name) return { class: "bg-gray-600", style: "" }; let found = false; if (checkGrid) { grid.forEach((row) => diff --git a/src/components/TopToolbar/TopToolbar.tsx b/src/components/TopToolbar/TopToolbar.tsx index a449486..9430ce0 100644 --- a/src/components/TopToolbar/TopToolbar.tsx +++ b/src/components/TopToolbar/TopToolbar.tsx @@ -4,28 +4,57 @@ import { GameStateContext } from "../GameStateProvider/GameStateProvider"; import { ToolbarButton } from "../Button/Button"; import { DoubleEndedArrow } from "./DoubleEndedArrow"; import { ReloadIcon } from "@radix-ui/react-icons"; +import { + OrientationActionType, + ReflectionDirection, + RotationDirection, +} from "../GameStateProvider/currentPentominoReducer"; const TopToolbar = ({ ...rest }) => { - const { rotateLeft, rotateRight, reflectX, reflectY } = useContext(GameStateContext); + const { orientationDispatch } = useContext(GameStateContext); return ( - + { + orientationDispatch({ type: OrientationActionType.rotate, direction: RotationDirection.Left }); + }} + aria-label="Rotate Left" + className="flex flex-row items-center gap-1" + >
Rotate Left
- + { + orientationDispatch({ type: OrientationActionType.rotate, direction: RotationDirection.Right }); + }} + aria-label="Rotate Right" + className="flex flex-row items-center gap-1" + > Rotate Right - + { + orientationDispatch({ type: OrientationActionType.reflect, direction: ReflectionDirection.X }); + }} + aria-label="Reflect X" + className="flex flex-row items-center" + >
Reflect X
- + { + orientationDispatch({ type: OrientationActionType.reflect, direction: ReflectionDirection.Y }); + }} + aria-label="Reflect Y" + className="flex flex-row items-center gap-1" + > Reflect Y diff --git a/src/components/Wordmark/Wordmark.tsx b/src/components/Wordmark/Wordmark.tsx index a9c2b34..a332279 100644 --- a/src/components/Wordmark/Wordmark.tsx +++ b/src/components/Wordmark/Wordmark.tsx @@ -75,7 +75,7 @@ export const Wordmark = ({ gridArea }: { gridArea: string }) => { (DEFAULT_COLORS[p] = 0)); -export enum Orientation { +export enum OrientabilityType { Orientable, Nonorientable, ConsecutiveOrientable, @@ -95,16 +99,16 @@ export enum Orientation { None, } -interface SurfaceOrientation { - h: Orientation; - w: Orientation; +interface Orientability { + h: OrientabilityType; + w: OrientabilityType; } export interface Surface { name: string; consecutive: boolean; key: string; - orientation: SurfaceOrientation; + orientability: Orientability; } interface Surfaces { @@ -116,43 +120,43 @@ export const SURFACES: Surfaces = { name: "Rectangle", consecutive: false, key: "R", - orientation: { w: Orientation.None, h: Orientation.None }, + orientability: { w: OrientabilityType.None, h: OrientabilityType.None }, }, Cylinder: { name: "Cylinder", consecutive: false, key: "C", - orientation: { w: Orientation.None, h: Orientation.Orientable }, + orientability: { w: OrientabilityType.None, h: OrientabilityType.Orientable }, }, Sphere: { name: "Sphere", consecutive: true, key: "S", - orientation: { w: Orientation.ConsecutiveOrientable, h: Orientation.ConsecutiveOrientable }, + orientability: { w: OrientabilityType.ConsecutiveOrientable, h: OrientabilityType.ConsecutiveOrientable }, }, Torus: { name: "Torus", consecutive: false, key: "T", - orientation: { w: Orientation.Orientable, h: Orientation.Orientable }, + orientability: { w: OrientabilityType.Orientable, h: OrientabilityType.Orientable }, }, Mobius: { name: "Mobius", consecutive: false, key: "M", - orientation: { w: Orientation.None, h: Orientation.Nonorientable }, + orientability: { w: OrientabilityType.None, h: OrientabilityType.Nonorientable }, }, ProjectivePlane: { name: "ProjectivePlane", consecutive: false, key: "P", - orientation: { w: Orientation.Nonorientable, h: Orientation.Nonorientable }, + orientability: { w: OrientabilityType.Nonorientable, h: OrientabilityType.Nonorientable }, }, KleinBottle: { name: "KleinBottle", consecutive: false, key: "K", - orientation: { w: Orientation.Orientable, h: Orientation.Nonorientable }, + orientability: { w: OrientabilityType.Orientable, h: OrientabilityType.Nonorientable }, }, }; @@ -219,10 +223,8 @@ export const randomPentominoColors = (numVisibleColors: number): Colors => { interface ActionPentomino { prevName: string; - prevRotation: number; - prevReflection: number; - x: number; - y: number; + prevOrientation: Orientation; + prevCoordinates: Coordinates; } export interface Action { diff --git a/src/pentominoes.ts b/src/pentominoes.ts index e22fc5a..50d9d0b 100644 --- a/src/pentominoes.ts +++ b/src/pentominoes.ts @@ -6,18 +6,18 @@ export interface Coordinates { y: number; } -interface Orientations { - [key: number]: Orientation[]; -} - -export interface Orientation { +export interface Shape { center: Coordinates; shape: number[][]; } +interface Shapes { + [key: number]: Shape[]; +} + export interface Pentomino { name: string; - orientations: Orientations; + shapes: Shapes; } interface Pentominoes { @@ -140,12 +140,12 @@ pentominoPrimitives.map((p) => { const reflectedShape = reflectX(p.shape); const expandedPentomino: Pentomino = { name: p.name, - orientations: { + shapes: { 0: [{ center: center(p.shape), shape: p.shape }], 1: [{ center: center(reflectedShape), shape: reflectedShape }], }, }; - const orientations = expandedPentomino.orientations; + const orientations = expandedPentomino.shapes; range(0, 3).forEach(() => { // rotate 3 times range(0, 2).forEach((i) => {