Skip to content

Commit

Permalink
Merge pull request #145 from webdevcody/spawn-more-zombies
Browse files Browse the repository at this point in the history
Spawn more zombies
  • Loading branch information
webdevcody authored Nov 5, 2024
2 parents 8ec749d + f294a4b commit 0011c12
Show file tree
Hide file tree
Showing 12 changed files with 236 additions and 51 deletions.
9 changes: 9 additions & 0 deletions app/multiplayer/[multiplayerGameId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,22 @@ export default function MultiplayerPage({
return <div>Game not found.</div>;
}

const playerLabels = multiplayerGame.playerMap.reduce(
(acc, { playerToken, modelSlug }) => {
acc[playerToken] = modelSlug;
return acc;
},
{} as Record<string, string>,
);

return (
<Page>
<PageTitle>Multiplayer</PageTitle>
<div className="flex justify-center">
<Visualizer
controls={false}
map={multiplayerGame.boardState}
playerLabels={playerLabels}
simulatorOptions={{ multiplayer: true }}
/>
</div>
Expand Down
14 changes: 11 additions & 3 deletions components/Renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,28 @@ export function useRenderer(
canvas: React.MutableRefObject<HTMLCanvasElement | null>,
cellSize: number = 64,
replaySpeed: number = DEFAULT_REPLAY_SPEED,
playerLabels?: Record<string, string>,
) {
const [renderer, setRenderer] = useState<Renderer | null>(null);

useEffect(() => {
if (map === null || map === undefined || canvas.current === null) {
if (!map || !canvas.current) {
return;
}

const renderer = new Renderer(map, canvas.current, cellSize, replaySpeed);
const renderer = new Renderer(
map,
canvas.current,
cellSize,
replaySpeed,
playerLabels,
);

void renderer.initialize().then(() => {
console.log("renderer initialized");
setRenderer(renderer);
});
}, [map, cellSize, replaySpeed]);
}, [map, cellSize, replaySpeed, playerLabels]);

return renderer;
}
12 changes: 10 additions & 2 deletions components/Visualizer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export function Visualizer({
map,
onReset,
onSimulationEnd,
playerLabels,
replaySpeed = DEFAULT_REPLAY_SPEED,
simulatorOptions,
}: {
Expand All @@ -25,6 +26,7 @@ export function Visualizer({
map: string[][];
onReset?: () => unknown;
onSimulationEnd?: (isWin: boolean) => unknown;
playerLabels?: Record<string, string>;
replaySpeed?: number;
simulatorOptions?: ZombieSurvivalOptions;
}) {
Expand All @@ -35,7 +37,13 @@ export function Visualizer({
const interval = useRef<ReturnType<typeof setTimeout> | null>(null);
const timeout = useRef<ReturnType<typeof setTimeout> | null>(null);
const canvas = useRef<HTMLCanvasElement | null>(null);
const renderer = useRenderer(map, canvas, cellSize, replaySpeed);
const renderer = useRenderer(
map,
canvas,
cellSize,
replaySpeed,
playerLabels,
);
const visible = useRef(false);
const [running, setRunning] = useState(false);

Expand Down Expand Up @@ -85,7 +93,7 @@ export function Visualizer({
setRunning(false);

if (onSimulationEnd) {
onSimulationEnd(!simulator.current.getPlayer().dead());
onSimulationEnd(!simulator.current.getPlayer()?.dead());
}
}, replaySpeed);
}
Expand Down
153 changes: 114 additions & 39 deletions convex/multiplayerGames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,64 @@ import { ZombieSurvival, fromDirectionString, move } from "../simulator";
import { v } from "convex/values";
import { api, internal } from "./_generated/api";
import { internalAction, internalMutation, query } from "./_generated/server";
import { AI_MODELS, ModelSlug } from "./constants";
import { ModelSlug } from "./constants";

const HARD_CODED_PLAYER_TOKEN = "1";
const TURN_DELAY = 0;
const TURN_DELAY = 500;

const boardState = `
Z.Z.Z. . . .B. . . . . . . . ,
Z.Z. . . . .B. . . . . . . . ,
Z. . . . .B. .1. . . . . . . ,
. . . . . .B. . . . . . . . ,
. . . . . .B. . . . . . . . ,
. . . . .B. . . . . . . . . ,
. . . .R. . . . .R. . . . . ,
. . . .R. . . . .R. . . . . ,
. . . .R. . . . .R. . . . . ,
. . . . . . . .B. . . . . .Z,
. . . . . . .B. . . . . .Z.Z,
. . . . . . .B. . . . .Z.Z.Z,
. . . . . . . .B. . . . . . ,
. . . . . . .B. . . . . . . ,
. . . . . . .B. . . . . . . ,
`;

export const startMultiplayerGame = internalMutation({
handler: async (ctx) => {
args: {
playerMap: v.array(
v.object({
modelSlug: v.string(),
playerToken: v.string(),
}),
),
},
handler: async (ctx, args) => {
const initialBoard = boardState
.trim()
.split(",\n")
.map((it) => it.split("."));

console.log({ initialBoard });

// spawn random players on the board
for (const player of args.playerMap) {
while (true) {
const x = Math.floor(Math.random() * initialBoard[0].length);
const y = Math.floor(Math.random() * initialBoard.length);

if (initialBoard[y][x] === " ") {
initialBoard[y][x] = player.playerToken;
break;
}
}
}

const gameId = await ctx.db.insert("multiplayerGames", {
boardState: boardState
.trim()
.split(",\n")
.map((it) => it.split(".")),
playerMap: [
{
modelSlug: AI_MODELS["gpt-4o"].slug,
playerToken: HARD_CODED_PLAYER_TOKEN,
},
],
boardState: initialBoard,
playerMap: args.playerMap,
completedTurns: 0,
});

await ctx.scheduler.runAfter(
0,
internal.multiplayerGames.runMultiplayerGameTurn,
{
multiplayerGameId: gameId,
turn: HARD_CODED_PLAYER_TOKEN,
turn: args.playerMap[0].playerToken,
},
);

Expand All @@ -61,9 +81,13 @@ export const updateMultiplayerGameBoardState = internalMutation({
args: {
multiplayerGameId: v.id("multiplayerGames"),
boardState: v.array(v.array(v.string())),
completedTurns: v.number(),
},
handler: async (ctx, args) => {
await ctx.db.patch(args.multiplayerGameId, { boardState: args.boardState });
await ctx.db.patch(args.multiplayerGameId, {
boardState: args.boardState,
completedTurns: args.completedTurns,
});
},
});

Expand All @@ -89,19 +113,31 @@ export const runMultiplayerGameTurn = internalAction({
const map = new ZombieSurvival(multiplayerGame.boardState);

if (turn === "Z") {
const numPlayers = multiplayerGame.playerMap.length;
let zombiesToSpawn = 1;
if (numPlayers === 1) {
zombiesToSpawn = 1;
} else if (numPlayers === 2) {
zombiesToSpawn = 2;
} else if (numPlayers === 3) {
zombiesToSpawn = 2;
} else if (numPlayers === 4) {
zombiesToSpawn = 3;
}
for (let i = 0; i < zombiesToSpawn; i++) {
map.spawnRandomZombie();
}
map.stepZombies();

await ctx.runMutation(
internal.multiplayerGames.updateMultiplayerGameBoardState,
{
multiplayerGameId,
boardState: map.getState(),
completedTurns: multiplayerGame.completedTurns + 1,
},
);
} else if (
ZombieSurvival.mapHasToken(map.getState(), turn) &&
turn === HARD_CODED_PLAYER_TOKEN
) {
} else {
const model = multiplayerGame.playerMap.find(
(entry) => entry.playerToken === turn,
);
Expand All @@ -110,6 +146,31 @@ export const runMultiplayerGameTurn = internalAction({
throw new Error("Model not found");
}

const player = map.getPlayer(turn);
if (!player) {
const currentPlayerIndex = multiplayerGame.playerMap.findIndex(
(entry) => entry.playerToken === turn,
);
const nextPlayerIndex = currentPlayerIndex + 1;
let nextPlayer: string;
if (nextPlayerIndex >= multiplayerGame.playerMap.length) {
nextPlayer = "Z";
} else {
nextPlayer = multiplayerGame.playerMap[nextPlayerIndex].playerToken;
}

await ctx.scheduler.runAfter(
0,
internal.multiplayerGames.runMultiplayerGameTurn,
{
multiplayerGameId,
turn: nextPlayer,
},
);

return;
}

const results = await runMultiplayerModel(
model.modelSlug as ModelSlug,
map.getState(),
Expand All @@ -118,17 +179,18 @@ export const runMultiplayerGameTurn = internalAction({

if (results.moveDirection && results.moveDirection !== "STAY") {
const moveDirection = fromDirectionString(results.moveDirection);
const movePosition = move(
map.getPlayer(turn).getPosition(),
moveDirection,
);

if (
map.isValidPosition(movePosition) &&
map.isPositionEmpty(movePosition)
) {
// only move if the position was valid, otherwise we don't move
map.getPlayer(turn).moveTo(movePosition);
const p = map.getPlayer(turn);

if (p) {
const movePosition = move(p.getPosition(), moveDirection);

if (
map.isValidPosition(movePosition) &&
map.isPositionEmpty(movePosition)
) {
// only move if the position was valid, otherwise we don't move
p.moveTo(movePosition);
}
}
}

Expand All @@ -142,17 +204,30 @@ export const runMultiplayerGameTurn = internalAction({
{
multiplayerGameId,
boardState: map.getState(),
completedTurns: multiplayerGame.completedTurns,
},
);
}

if (!map.finished()) {
if (!map.allPlayersDead()) {
let nextPlayer: string;

const currentPlayerIndex = multiplayerGame.playerMap.findIndex(
(entry) => entry.playerToken === turn,
);
const nextPlayerIndex = currentPlayerIndex + 1;
if (nextPlayerIndex >= multiplayerGame.playerMap.length) {
nextPlayer = "Z";
} else {
nextPlayer = multiplayerGame.playerMap[nextPlayerIndex].playerToken;
}

await ctx.scheduler.runAfter(
TURN_DELAY,
internal.multiplayerGames.runMultiplayerGameTurn,
{
multiplayerGameId,
turn: turn === "Z" ? HARD_CODED_PLAYER_TOKEN : "Z",
turn: nextPlayer,
},
);
}
Expand Down
1 change: 1 addition & 0 deletions convex/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export default defineSchema({
}).index("by_active", ["isActive"]),
multiplayerGames: defineTable({
boardState: v.array(v.array(v.string())),
completedTurns: v.number(),
playerMap: v.array(
v.object({
modelSlug: v.string(),
Expand Down
6 changes: 3 additions & 3 deletions lib/prepareCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ export function prepareCanvas(
throw new Error("Unable to get 2d context");
}

canvas.height = height * window.devicePixelRatio;
canvas.width = width * window.devicePixelRatio;
canvas.height = height;
canvas.width = width;
canvas.style.height = `${height}px`;
canvas.style.width = `${width}px`;

ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
// ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
return ctx;
}
3 changes: 3 additions & 0 deletions models/multiplayer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,12 @@ export async function runMultiplayerModel(
"STAY",
];

const zombieLocations = ZombieSurvival.getZombieLocations(map);

const userPrompt =
`Grid: ${JSON.stringify(map)}\n\n` +
`Your Player Token: ${playerToken}\n\n` +
`Zombie Locations: ${JSON.stringify(zombieLocations)}\n\n` +
`Valid Move Locations: ${JSON.stringify(validDirections)}`;

let result;
Expand Down
11 changes: 11 additions & 0 deletions renderer/Item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,28 @@ export class RendererItem {
height: number;
position: Position;
width: number;
label?: string;

constructor(
data: HTMLImageElement | string,
position: Position,
width: number,
height: number,
label?: string,
) {
this.data = data;
this.height = height;
this.position = position;
this.width = width;
this.label = label;
}

public getDisplayName(): string {
return this.label ?? "";
}

public hasDisplayName(): boolean {
return this.label !== undefined;
}

public addEffect(...effects: RendererEffect[]) {
Expand Down
Loading

0 comments on commit 0011c12

Please sign in to comment.