Skip to content

Commit

Permalink
Record multiplayer game actions to convex
Browse files Browse the repository at this point in the history
  • Loading branch information
delasy committed Nov 7, 2024
1 parent cfa66a9 commit 3752bef
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 127 deletions.
2 changes: 2 additions & 0 deletions convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type * as constants from "../constants.js";
import type * as crons from "../crons.js";
import type * as flags from "../flags.js";
import type * as games from "../games.js";
import type * as helpers from "../helpers.js";
import type * as http from "../http.js";
import type * as init from "../init.js";
import type * as leaderboard from "../leaderboard.js";
Expand Down Expand Up @@ -49,6 +50,7 @@ declare const fullApi: ApiFromModules<{
crons: typeof crons;
flags: typeof flags;
games: typeof games;
helpers: typeof helpers;
http: typeof http;
init: typeof init;
leaderboard: typeof leaderboard;
Expand Down
7 changes: 7 additions & 0 deletions convex/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ export const AI_MODELS = {
},
} as const;

export enum ActionType {
PlayerShoot = "player-shoot",
PlayerWalk = "player-walk",
ZombieSpawn = "zombie-spawn",
ZombieStep = "zombie-step",
}

export type ModelSlug = (typeof AI_MODELS)[keyof typeof AI_MODELS]["slug"];

export const AI_MODEL_SLUGS = Object.keys(AI_MODELS) as ModelSlug[];
Expand Down
19 changes: 19 additions & 0 deletions convex/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { v } from "convex/values";
import { ActionType } from "./constants";

export const multiplayerGameActionValidator = v.object({
// type: v.union(...Object.values(ActionType).map((t) => v.literal(t))),
type: v.union(
v.literal("player-shoot"),
v.literal("player-walk"),
v.literal("zombie-spawn"),
v.literal("zombie-step"),
),
token: v.string(),
position: v.optional(
v.object({
x: v.number(),
y: v.number(),
}),
),
});
220 changes: 119 additions & 101 deletions convex/multiplayerGames.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { runMultiplayerModel } from "../models/multiplayer";
import { ZombieSurvival, fromDirectionString, move } from "../simulator";
import { v } from "convex/values";
import {
Position,
ZombieSurvival,
directionFromString,
move,
} from "../simulator";
import { type Infer, v } from "convex/values";
import { api, internal } from "./_generated/api";
import { Doc } from "./_generated/dataModel";
import { internalAction, internalMutation, query } from "./_generated/server";
import { ModelSlug } from "./constants";
import { ActionType, ModelSlug } from "./constants";
import { multiplayerGameActionValidator } from "./helpers";

const TURN_DELAY = 500;

Expand All @@ -19,6 +26,32 @@ const boardState = `
. . . . . . .B. . . . . . . ,
`;

function multiplayerGameTurn(multiplayerGame: Doc<"multiplayerGames">): string {
const prevAction = multiplayerGame.actions.at(-1);

if (prevAction === undefined) {
return multiplayerGame.playerMap[0].playerToken;
}

const players = multiplayerGame.playerMap;
const simulator = new ZombieSurvival(multiplayerGame.boardState);

const playerIndex = players.findIndex(
(player) => player.playerToken === prevAction.token,
);

for (let i = playerIndex + 1; i < players.length; i++) {
const turn = players[i].playerToken;
const player = simulator.getPlayer(turn);

if (player !== undefined) {
return turn;
}
}

return "Z";
}

export const startMultiplayerGame = internalMutation({
args: {
playerMap: v.array(
Expand Down Expand Up @@ -50,18 +83,16 @@ export const startMultiplayerGame = internalMutation({
}

const gameId = await ctx.db.insert("multiplayerGames", {
map: initialBoard,
boardState: initialBoard,
playerMap: args.playerMap,
completedTurns: 0,
actions: [],
});

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

return gameId;
Expand All @@ -81,68 +112,74 @@ export const updateMultiplayerGameBoardState = internalMutation({
args: {
multiplayerGameId: v.id("multiplayerGames"),
boardState: v.array(v.array(v.string())),
completedTurns: v.number(),
cost: v.optional(v.number()),
actions: v.array(multiplayerGameActionValidator),
},
handler: async (ctx, args) => {
const patch: {
boardState: string[][];
completedTurns: number;
cost?: number;
} = {
boardState: args.boardState,
completedTurns: args.completedTurns,
};

if (args.cost !== undefined) {
patch.cost = args.cost;
const { actions, boardState, cost, multiplayerGameId } = args;

const multiplayerGame = await ctx.runQuery(
api.multiplayerGames.getMultiplayerGame,
{ multiplayerGameId },
);

if (!multiplayerGame) {
throw new Error("Multiplayer game not found");
}

await ctx.db.patch(args.multiplayerGameId, patch);
await ctx.db.patch(multiplayerGame._id, {
boardState: boardState,
cost: cost ?? multiplayerGame.cost,
actions: [...multiplayerGame.actions, ...actions],
});
},
});

export const runMultiplayerGameTurn = internalAction({
args: {
turn: v.string(),
multiplayerGameId: v.id("multiplayerGames"),
},
handler: async (ctx, args) => {
const { turn, multiplayerGameId } = args;
const { multiplayerGameId } = args;

const multiplayerGame = await ctx.runQuery(
api.multiplayerGames.getMultiplayerGame,
{
multiplayerGameId,
},
{ multiplayerGameId },
);

if (!multiplayerGame) {
throw new Error("Multiplayer game not found");
}

const map = new ZombieSurvival(multiplayerGame.boardState);
const simulator = new ZombieSurvival(multiplayerGame.boardState);
const turn = multiplayerGameTurn(multiplayerGame);
const actions: Array<Infer<typeof multiplayerGameActionValidator>> = [];
let cost = multiplayerGame.cost ?? 0;

if (turn === "Z") {
map.stepZombies();
simulator.stepZombies();

actions.push({
type: ActionType.ZombieStep,
token: "Z",
});

const numPlayers = multiplayerGame.playerMap.length;

const zombiesToSpawn = Math.min(
Math.floor(Math.random() * numPlayers) + 1,
numPlayers,
);
for (let i = 0; i < zombiesToSpawn; i++) {
map.spawnRandomZombie();
}

await ctx.runMutation(
internal.multiplayerGames.updateMultiplayerGameBoardState,
{
multiplayerGameId,
boardState: map.getState(),
completedTurns: multiplayerGame.completedTurns + 1,
},
);
for (let i = 0; i < zombiesToSpawn && simulator.hasEmptyCells(); i++) {
const position = simulator.spawnRandomZombie();

actions.push({
type: ActionType.ZombieSpawn,
token: "Z",
position,
});
}
} else {
const model = multiplayerGame.playerMap.find(
(entry) => entry.playerToken === turn,
Expand All @@ -152,92 +189,73 @@ 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(),
simulator.getState(),
turn,
);

console.log("cost", results.cost);

if (results.moveDirection && results.moveDirection !== "STAY") {
const moveDirection = fromDirectionString(results.moveDirection);
const p = map.getPlayer(turn);
const moveDirection = directionFromString(results.moveDirection);
const player = simulator.getPlayer(turn);

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

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

actions.push({
type: ActionType.PlayerWalk,
token: turn,
position: movePosition,
});
}
}
}

if (results.zombieToShoot) {
const zombieToShoot = results.zombieToShoot;
map.getZombieAt({ x: zombieToShoot[1], y: zombieToShoot[0] })?.hit();
}
if (results.zombieToShoot !== undefined) {
const zombiePosition: Position = {
x: results.zombieToShoot[1],
y: results.zombieToShoot[0],
};

await ctx.runMutation(
internal.multiplayerGames.updateMultiplayerGameBoardState,
{
multiplayerGameId,
boardState: map.getState(),
completedTurns: multiplayerGame.completedTurns,
cost: (multiplayerGame.cost ?? 0) + (results.cost ?? 0),
},
);
}
const zombie = simulator.getZombieAt(zombiePosition);

if (!map.allPlayersDead()) {
let nextPlayer: string;
if (zombie !== undefined) {
zombie.hit();

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;
actions.push({
type: ActionType.PlayerShoot,
token: turn,
position: zombiePosition,
});
}
}

cost += results.cost ?? 0;
}

await ctx.runMutation(
internal.multiplayerGames.updateMultiplayerGameBoardState,
{
multiplayerGameId,
boardState: simulator.getState(),
actions,
cost,
},
);

if (!simulator.allPlayersDead()) {
await ctx.scheduler.runAfter(
TURN_DELAY,
internal.multiplayerGames.runMultiplayerGameTurn,
{
multiplayerGameId,
turn: nextPlayer,
},
{ multiplayerGameId },
);
}
},
Expand Down
4 changes: 3 additions & 1 deletion convex/schema.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { authTables } from "@convex-dev/auth/server";
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
import { multiplayerGameActionValidator } from "./helpers";

// The schema is normally optional, but Convex Auth
// requires indexes defined on `authTables`.
Expand Down Expand Up @@ -82,9 +83,10 @@ export default defineSchema({
isActive: v.boolean(),
}).index("by_active", ["isActive"]),
multiplayerGames: defineTable({
map: v.array(v.array(v.string())),
boardState: v.array(v.array(v.string())),
completedTurns: v.number(),
cost: v.optional(v.number()),
actions: v.array(multiplayerGameActionValidator),
playerMap: v.array(
v.object({
modelSlug: v.string(),
Expand Down
Loading

0 comments on commit 3752bef

Please sign in to comment.