Skip to content

Commit

Permalink
FRS-4 API (#4)
Browse files Browse the repository at this point in the history
* Updated documentation

* Added working server action

* Updated hexgrid state

* Added remaining algorithms and updated test suite to be a single parameterised test.
  • Loading branch information
stuart-bradley authored Feb 19, 2024
1 parent 136fecd commit 6b2bc34
Show file tree
Hide file tree
Showing 15 changed files with 296 additions and 77 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Full documentation can be found in the [documentation](documentation) folder.
- [React](https://react.dev/) `18.*`
- [react-hexgrid](https://github.com/Hellenic/react-hexgrid) `2.0.0@beta`
- [emotion](https://emotion.sh/docs/introduction) `11.11.3` (dependency not imported by react-hexgrid)
- [Zod](https://zod.dev/) `3.22.4`

### Dev Dependencies

Expand Down
26 changes: 25 additions & 1 deletion documentation/functional_requirements_specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,33 @@ tiles, see Appendix C.

### 4 API

The API for this project consists of a single endpoint to handle the generate form submission.

### 4.1 API Implementation

### 4.2 Board Unique Identifier
Next.js suggests using [Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations),
instead of the older Node.js `/api/` endpoints.

The endpoint should: validate incoming form data using [Zod](https://zod.dev/); pass the validated data to the Board
Generation Algorithm (described in 3); and then redirect back to a URL with a Unique Board Identifier (described in 4.2).

### 4.2 Unique Board Identifier

To be able to save and share boards, each board needs a unique identifier encoded in the URL. Because the Board
Generation Algorithm (described in 3) uses unique characters for each tile type, an encoded board can look as follows:

```
RST-WBSB-WTDTR-TRWS-BWS
```

Each block of letters describes a row of the board, so the middle block is the largest, and decreases by 1 for each row
further out.

This identifier could easily be extended to include additional metadata, and even allows a user to create or modify a board
solely from the identifier.

To render the identifier, it can be pulled from the URL using Next.js [useParams](https://nextjs.org/docs/app/api-reference/functions/use-params)
functionality.

### 5 Testing

Expand Down
11 changes: 10 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"react": "^18",
"react-dom": "^18",
"react-hexgrid": "^2.0.0-beta.4",
"react-hook-form": "^7.50.1"
"react-hook-form": "^7.50.1",
"zod": "^3.22.4"
},
"devDependencies": {
"@testing-library/react": "^14.2.1",
Expand Down
29 changes: 27 additions & 2 deletions src/actions/actions.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,34 @@
"use server";

import { redirect } from "next/navigation";
import { z } from "zod";
import { ALGORITHM_ARRAY, PLAYERS_4, PLAYERS_6 } from "@/lib/constants";
import CatanBoardGenerator from "@/lib/CatanBoardGenerator";

const schema = z.object({
useSeafarers: z.boolean(),
randAlgorithm: z.enum(ALGORITHM_ARRAY),
numOfPlayers: z.enum([PLAYERS_4, PLAYERS_6]),
});

export async function generateBoard(formData: FormData) {
const serializedBoard = "RST-WBSB-WTDTR-TRWS-BWS";
console.log(formData);
const validatedFields = schema.safeParse({
useSeafarers: formData.has("useSeafarers"),
randAlgorithm: formData.get("randAlgorithm"),
numOfPlayers: formData.get("numOfPlayers"),
});

if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
};
}
// const serializedBoard = "RST-WBSB-WTDTR-TRWS-BWS";

const serializedBoard = new CatanBoardGenerator(
validatedFields.data.useSeafarers,
validatedFields.data.numOfPlayers,
validatedFields.data.randAlgorithm,
).toString();
redirect(`/${serializedBoard}`);
}
12 changes: 12 additions & 0 deletions src/app/[board]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import BoardGrid from "@/app/components/ui/BoardGrid";
import BoardForm from "@/app/components/ui/BoardForm";
import * as React from "react";

export default function Page() {
return (
<>
<BoardGrid />
<BoardForm />
</>
);
}
3 changes: 0 additions & 3 deletions src/app/[catan-board]/page.tsx

This file was deleted.

3 changes: 3 additions & 0 deletions src/app/components/ui/BoardForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import {
PLAYERS_4,
} from "@/lib/constants";
import { generateBoard } from "@/actions/actions";
import { useFormStatus } from "react-dom";

const BoardForm: React.FC = () => {
const { register, watch } = useForm();
const watchUseSeafarers = watch("useSeafarers");
const [getAlgorithms, setAlgorithms] = useState(ALGORITHMS_BASE);
const { pending } = useFormStatus();

useEffect(() => {
if (watchUseSeafarers === true) {
Expand Down Expand Up @@ -85,6 +87,7 @@ const BoardForm: React.FC = () => {
</fieldset>
<button
type="submit"
aria-disabled={pending}
className="px-6 py-3.5 text-base w-full font-medium text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
>
Generate
Expand Down
25 changes: 17 additions & 8 deletions src/app/components/ui/BoardGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,26 @@

import { HexGrid, Layout, Hexagon } from "react-hexgrid";
import { getCubeCoords } from "@/lib/utils";
import React from "react";

const serializedBoard = "RST-WBSB-WTDTR-TRWS-BWS";
import React, { useState } from "react";
import { useParams } from "next/navigation";

const BoardGrid: React.FC = () => {
const hexagons = getCubeCoords(serializedBoard);

const params = useParams<{ board: string }>();
const [getHexagons] = useState(getCubeCoords(params.board));
// Size and origin are dependent on grid size it seems.
return (
<HexGrid className="max-w-fit mx-auto">
<Layout flat={false} spacing={1.02}>
{hexagons.map(({ hex, style }, i) => (
<HexGrid
className="max-w-fit mx-auto"
width={window.innerWidth}
height="500"
>
<Layout
size={{ x: 6, y: 6 }}
origin={{ x: -15, y: 0 }}
flat={false}
spacing={1.02}
>
{getHexagons.map(({ hex, style }, i) => (
<Hexagon key={i} q={hex.q} r={hex.r} s={hex.s} style={style} />
))}
</Layout>
Expand Down
5 changes: 2 additions & 3 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import React from "react";

const inter = Inter({ subsets: ["latin"] });

Expand All @@ -17,9 +18,7 @@ export default function RootLayout({
return (
<html lang="en">
<body className={inter.className}>
<div className="w-full p-4 bg-white border border-gray-200 rounded-lg shadow sm:p-8 dark:bg-gray-800 dark:border-gray-700">
{children}
</div>
<div className="w-full p-4 bg-white">{children}</div>
</body>
</html>
);
Expand Down
8 changes: 1 addition & 7 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import BoardGrid from "@/app/components/ui/BoardGrid";
import BoardForm from "@/app/components/ui/BoardForm";
import * as React from "react";

export default function Home() {
return (
<>
<BoardGrid />
<BoardForm />
</>
);
return <BoardForm />;
}
127 changes: 101 additions & 26 deletions src/lib/CatanBoardGenerator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,110 @@ import { expect, test } from "vitest";
import CatanBoardGenerator from "./CatanBoardGenerator";
import {
ALGORITHM_COASTAL,
ALGORITHM_LARGE_LAND_MASS,
ALGORITHM_RANDOM,
ALGORITHM_SMALL_ISLANDS,
ALGORITHM_LARGE_ISLANDS,
ALGORITHM_THIN_LAND_MASS,
PLAYERS_4,
PLAYERS_6,
} from "./constants";

test("Test CatanBoardGenerator returns correct serialised shape for the base 4 player game", () => {
const board = new CatanBoardGenerator(false, PLAYERS_4, ALGORITHM_RANDOM);
const serialisedBoard = board.toString();
expect(serialisedBoard).toMatch(
/^[WRSTBD]{3}-[WRSTBD]{4}-[WRSTBD]{5}-[WRSTBD]{4}-[WRSTBD]{3}$/,
);
});
const player4BaseRegex = new RegExp(
/^[WRSTBD]{3}-[WRSTBD]{4}-[WRSTBD]{5}-[WRSTBD]{4}-[WRSTBD]{3}$/,
);
const player6BaseRegex = new RegExp(
/^[WRSTBD]{3}-[WRSTBD]{4}-[WRSTBD]{5}-[WRSTBD]{6}-[WRSTBD]{5}-[WRSTBD]{4}-[WRSTBD]{3}$/,
);
const player4SeafarersRegex = new RegExp(
/^[WRSTBDGO]{4}-[WRSTBDGO]{5}-[WRSTBDGO]{6}-[WRSTBDGO]{7}-[WRSTBDGO]{6}-[WRSTBDGO]{5}-[WRSTBDGO]{4}$/,
);
const player6SeafarersRegex = new RegExp(
/^[WRSTBDGO]{7}-[WRSTBDGO]{8}-[WRSTBDGO]{9}-[WRSTBDGO]{10}-[WRSTBDGO]{9}-[WRSTBDGO]{8}-[WRSTBDGO]{7}$/,
);

test("Test CatanBoardGenerator returns correct serialised shape for the seafarers 4 player game coastal algorithm", () => {
const board = new CatanBoardGenerator(true, PLAYERS_4, ALGORITHM_COASTAL);
const serialisedBoard = board.toString();
expect(serialisedBoard).toMatch(
/^[WRSTBDGO]{4}-[WRSTBDGO]{5}-[WRSTBDGO]{6}-[WRSTBDGO]{7}-[WRSTBDGO]{6}-[WRSTBDGO]{5}-[WRSTBDGO]{4}$/,
);
});

test("Test CatanBoardGenerator returns correct serialised shape for the seafarers 4 player game thin land mass algorithm", () => {
const board = new CatanBoardGenerator(
true,
PLAYERS_4,
ALGORITHM_THIN_LAND_MASS,
);
const serialisedBoard = board.toString();
expect(serialisedBoard).toMatch(
/^[WRSTBDGO]{4}-[WRSTBDGO]{5}-[WRSTBDGO]{6}-[WRSTBDGO]{7}-[WRSTBDGO]{6}-[WRSTBDGO]{5}-[WRSTBDGO]{4}$/,
);
});
test.each([
{
useSeafarers: false,
numOfPlayers: PLAYERS_4,
algorithm: ALGORITHM_RANDOM,
expected: player4BaseRegex,
},
{
useSeafarers: false,
numOfPlayers: PLAYERS_6,
algorithm: ALGORITHM_RANDOM,
expected: player6BaseRegex,
},
{
useSeafarers: true,
numOfPlayers: PLAYERS_4,
algorithm: ALGORITHM_COASTAL,
expected: player4SeafarersRegex,
},
{
useSeafarers: true,
numOfPlayers: PLAYERS_6,
algorithm: ALGORITHM_COASTAL,
expected: player6SeafarersRegex,
},
{
useSeafarers: true,
numOfPlayers: PLAYERS_4,
algorithm: ALGORITHM_THIN_LAND_MASS,
expected: player4SeafarersRegex,
},
{
useSeafarers: true,
numOfPlayers: PLAYERS_6,
algorithm: ALGORITHM_THIN_LAND_MASS,
expected: player6SeafarersRegex,
},
{
useSeafarers: true,
numOfPlayers: PLAYERS_4,
algorithm: ALGORITHM_LARGE_LAND_MASS,
expected: player4SeafarersRegex,
},
{
useSeafarers: true,
numOfPlayers: PLAYERS_6,
algorithm: ALGORITHM_LARGE_LAND_MASS,
expected: player6SeafarersRegex,
},
{
useSeafarers: true,
numOfPlayers: PLAYERS_4,
algorithm: ALGORITHM_SMALL_ISLANDS,
expected: player4SeafarersRegex,
},
{
useSeafarers: true,
numOfPlayers: PLAYERS_6,
algorithm: ALGORITHM_SMALL_ISLANDS,
expected: player6SeafarersRegex,
},
{
useSeafarers: true,
numOfPlayers: PLAYERS_4,
algorithm: ALGORITHM_LARGE_ISLANDS,
expected: player4SeafarersRegex,
},
{
useSeafarers: true,
numOfPlayers: PLAYERS_6,
algorithm: ALGORITHM_LARGE_ISLANDS,
expected: player6SeafarersRegex,
},
])(
"Test CatanBoardGenerator returns correct serialised shape for a $numOfPlayers player game using $algorithm algorithm (Seafarers: $useSeafarers)",
({ useSeafarers, numOfPlayers, algorithm, expected }) => {
const board = new CatanBoardGenerator(
useSeafarers,
numOfPlayers,
algorithm,
);
const serialisedBoard = board.toString();
expect(serialisedBoard).toMatch(expected);
},
);
Loading

0 comments on commit 6b2bc34

Please sign in to comment.