import { Infer, v } from 'convex/values'; import { Doc, Id } from '../_generated/dataModel'; import { ActionCtx, DatabaseReader, MutationCtx, internalMutation, internalQuery, } from '../_generated/server'; import { World, serializedWorld } from './world'; import { WorldMap, serializedWorldMap } from './worldMap'; import { PlayerDescription, serializedPlayerDescription } from './playerDescription'; import { Location, locationFields, playerLocation } from './location'; import { runAgentOperation } from './agent'; import { GameId, IdTypes, allocGameId } from './ids'; import { InputArgs, InputNames, inputs } from './inputs'; import { AbstractGame, EngineUpdate, applyEngineUpdate, engineUpdate, loadEngine, } from '../engine/abstractGame'; import { internal } from '../_generated/api'; import { HistoricalObject } from '../engine/historicalObject'; import { AgentDescription, serializedAgentDescription } from './agentDescription'; import { parseMap, serializeMap } from '../util/object'; type WerewolfLookupTable = { [key: number]: number; }; const werewolfLookup: WerewolfLookupTable = { 8: 2, 9: 2, 10: 2, 11: 2, 12: 3, 13: 3, 14: 3, 15: 3, 16: 3, 17: 3, 18: 4 }; const gameState = v.object({ world: v.object(serializedWorld), playerDescriptions: v.array(v.object(serializedPlayerDescription)), agentDescriptions: v.array(v.object(serializedAgentDescription)), worldMap: v.object(serializedWorldMap), }); type GameState = Infer; const gameStateDiff = v.object({ world: v.object(serializedWorld), playerDescriptions: v.optional(v.array(v.object(serializedPlayerDescription))), agentDescriptions: v.optional(v.array(v.object(serializedAgentDescription))), worldMap: v.optional(v.object(serializedWorldMap)), agentOperations: v.array(v.object({ name: v.string(), args: v.any() })), }); type GameStateDiff = Infer; export class Game extends AbstractGame { tickDuration = 16; stepDuration = 1000; maxTicksPerStep = 600; maxInputsPerStep = 32; world: World; historicalLocations: Map, HistoricalObject>; descriptionsModified: boolean; worldMap: WorldMap; playerDescriptions: Map, PlayerDescription>; agentDescriptions: Map, AgentDescription>; pendingOperations: Array<{ name: string; args: any }> = []; numPathfinds: number; // winner?: 'werewolves' | 'villagers' | undefined constructor( engine: Doc<'engines'>, public worldId: Id<'worlds'>, state: GameState, ) { super(engine); this.world = new World(state.world); delete this.world.historicalLocations; this.descriptionsModified = false; this.worldMap = new WorldMap(state.worldMap); this.agentDescriptions = parseMap(state.agentDescriptions, AgentDescription, (a) => a.agentId); this.playerDescriptions = parseMap( state.playerDescriptions, PlayerDescription, (p) => p.playerId, ); this.historicalLocations = new Map(); this.numPathfinds = 0; } static async load( db: DatabaseReader, worldId: Id<'worlds'>, generationNumber: number, ): Promise<{ engine: Doc<'engines'>; gameState: GameState }> { const worldDoc = await db.get(worldId); if (!worldDoc) { throw new Error(`No world found with id ${worldId}`); } const worldStatus = await db .query('worldStatus') .withIndex('worldId', (q) => q.eq('worldId', worldId)) .unique(); if (!worldStatus) { throw new Error(`No engine found for world ${worldId}`); } const engine = await loadEngine(db, worldStatus.engineId, generationNumber); const playerDescriptionsDocs = await db .query('playerDescriptions') .withIndex('worldId', (q) => q.eq('worldId', worldId)) .collect(); const agentDescriptionsDocs = await db .query('agentDescriptions') .withIndex('worldId', (q) => q.eq('worldId', worldId)) .collect(); const worldMapDoc = await db .query('maps') .withIndex('worldId', (q) => q.eq('worldId', worldId)) .unique(); if (!worldMapDoc) { throw new Error(`No map found for world ${worldId}`); } // Discard the system fields and historicalLocations from the world state. const { _id, _creationTime, historicalLocations: _, ...world } = worldDoc; const playerDescriptions = playerDescriptionsDocs // Discard player descriptions for players that no longer exist. // .filter((d) => !!world.players.find((p) => p.id === d.playerId)) .map(({ _id, _creationTime, worldId: _, ...doc }) => doc); const agentDescriptions = agentDescriptionsDocs .filter((a) => !!world.agents.find((p) => p.id === a.agentId)) .map(({ _id, _creationTime, worldId: _, ...doc }) => doc); const { _id: _mapId, _creationTime: _mapCreationTime, worldId: _mapWorldId, ...worldMap } = worldMapDoc; return { engine, gameState: { world, playerDescriptions, agentDescriptions, worldMap, }, }; } allocId(idType: T): GameId { const id = allocGameId(idType, this.world.nextId); this.world.nextId += 1; return id; } scheduleOperation(name: string, args: unknown) { this.pendingOperations.push({ name, args }); } handleInput(now: number, name: Name, args: InputArgs) { const handler = inputs[name]?.handler; if (!handler) { throw new Error(`Invalid input: ${name}`); } return handler(this, now, args as any); } beginStep(_now: number) { // Store the current location of all players in the history tracking buffer. this.historicalLocations.clear(); for (const player of this.world.players.values()) { this.historicalLocations.set( player.id, new HistoricalObject(locationFields, playerLocation(player)), ); } this.numPathfinds = 0; } tick(now: number) { if (this.world.gameCycle.cycleState != 'EndGame') { // update game cycle counter this.world.gameCycle.tick(this, this.tickDuration); for (const player of this.world.players.values()) { player.tick(this, now); } for (const player of this.world.players.values()) { player.tickPathfinding(this, now); } for (const player of this.world.players.values()) { player.tickPosition(this, now); } for (const conversation of this.world.conversations.values()) { conversation.tick(this, now); } for (const agent of this.world.agents.values()) { agent.tick(this, now); } // Save each player's location into the history buffer at the end of // each tick. for (const player of this.world.players.values()) { let historicalObject = this.historicalLocations.get(player.id); if (!historicalObject) { historicalObject = new HistoricalObject(locationFields, playerLocation(player)); this.historicalLocations.set(player.id, historicalObject); } historicalObject.update(now, playerLocation(player)); } // Check for end game conditions // are there any humans? // we check for endgame if there's at least 1 human player const humans = [...this.world.players.values()].filter(player => player.human) if (humans.length > 0) { // all 'werewolf' are dead -> villagers win const werewolves = [...this.world.players.values()].filter(player => player.playerType(this) === 'werewolf' ) if (werewolves.length === 0) { // TODO finish game with villagers victory // console.log('villagers win') this.world.gameCycle.endgame(this) this.world.winner = 'villagers' } // just 1 'villager' left -> werewolves win const villagers = [...this.world.players.values()].filter(player => player.playerType(this) === 'villager' ) if (villagers.length <= 1) { // TODO finish game with werewolves victory // console.log('werewolves win') this.world.gameCycle.endgame(this) this.world.winner = 'werewolves' } } // debug // console.log(`we have ${ villagers.length } villagers`) // console.log(`we have ${ werewolves.length } werewolves`) // console.log(`we have ${ this.world.players.size } players`) // console.log(`we have ${ this.world.playersInit.size } initial players`) // console.log(`we have ${ humans.length } humans`) } } assignRoles() { const players = [...this.world.players.values()]; for (let i = players.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [players[i], players[j]] = [players[j], players[i]]; }; const werewolves = players.slice(0, werewolfLookup[players.length]); // mark as werewolves for (var wwolf of werewolves) { console.log(`player ${ wwolf.id } is a werewolf !`) const wwolfDescription = this.playerDescriptions.get(wwolf.id); if (wwolfDescription) { wwolfDescription.type = 'werewolf' } }; } async saveStep(ctx: ActionCtx, engineUpdate: EngineUpdate): Promise { const diff = this.takeDiff(); await ctx.runMutation(internal.aiTown.game.saveWorld, { engineId: this.engine._id, engineUpdate, worldId: this.worldId, worldDiff: diff, }); } takeDiff(): GameStateDiff { const historicalLocations = []; let bufferSize = 0; for (const [id, historicalObject] of this.historicalLocations.entries()) { const buffer = historicalObject.pack(); if (!buffer) { continue; } historicalLocations.push({ playerId: id, location: buffer }); bufferSize += buffer.byteLength; } if (bufferSize > 0) { console.debug( `Packed ${Object.entries(historicalLocations).length} history buffers in ${( bufferSize / 1024 ).toFixed(2)}KiB.`, ); } this.historicalLocations.clear(); const result: GameStateDiff = { world: { ...this.world.serialize(), historicalLocations }, agentOperations: this.pendingOperations, }; this.pendingOperations = []; if (this.descriptionsModified) { result.playerDescriptions = serializeMap(this.playerDescriptions); result.agentDescriptions = serializeMap(this.agentDescriptions); result.worldMap = this.worldMap.serialize(); this.descriptionsModified = false; } return result; } static async saveDiff(ctx: MutationCtx, worldId: Id<'worlds'>, diff: GameStateDiff) { const existingWorld = await ctx.db.get(worldId); if (!existingWorld) { throw new Error(`No world found with id ${worldId}`); } const newWorld = diff.world; // Archive newly deleted players, conversations, and agents. for (const player of existingWorld.players) { if (!newWorld.players.some((p) => p.id === player.id)) { await ctx.db.insert('archivedPlayers', { worldId, ...player }); } } for (const conversation of existingWorld.conversations) { if (!newWorld.conversations.some((c) => c.id === conversation.id)) { const participants = conversation.participants.map((p) => p.playerId); const archivedConversation = { worldId, id: conversation.id, created: conversation.created, creator: conversation.creator, ended: Date.now(), lastMessage: conversation.lastMessage, numMessages: conversation.numMessages, participants, }; await ctx.db.insert('archivedConversations', archivedConversation); for (let i = 0; i < participants.length; i++) { for (let j = 0; j < participants.length; j++) { if (i == j) { continue; } const player1 = participants[i]; const player2 = participants[j]; await ctx.db.insert('participatedTogether', { worldId, conversationId: conversation.id, player1, player2, ended: Date.now(), }); } } } } for (const conversation of existingWorld.agents) { if (!newWorld.agents.some((a) => a.id === conversation.id)) { await ctx.db.insert('archivedAgents', { worldId, ...conversation }); } } // Update the world state. await ctx.db.replace(worldId, newWorld); // Update the larger description tables if they changed. const { playerDescriptions, agentDescriptions, worldMap } = diff; if (playerDescriptions) { for (const description of playerDescriptions) { const existing = await ctx.db .query('playerDescriptions') .withIndex('worldId', (q) => q.eq('worldId', worldId).eq('playerId', description.playerId), ) .unique(); if (existing) { await ctx.db.replace(existing._id, { worldId, ...description }); } else { await ctx.db.insert('playerDescriptions', { worldId, ...description }); } } } if (agentDescriptions) { for (const description of agentDescriptions) { const existing = await ctx.db .query('agentDescriptions') .withIndex('worldId', (q) => q.eq('worldId', worldId).eq('agentId', description.agentId)) .unique(); if (existing) { await ctx.db.replace(existing._id, { worldId, ...description }); } else { await ctx.db.insert('agentDescriptions', { worldId, ...description }); } } } if (worldMap) { const existing = await ctx.db .query('maps') .withIndex('worldId', (q) => q.eq('worldId', worldId)) .unique(); if (existing) { await ctx.db.replace(existing._id, { worldId, ...worldMap }); } else { await ctx.db.insert('maps', { worldId, ...worldMap }); } } // Start the desired agent operations. for (const operation of diff.agentOperations) { await runAgentOperation(ctx, operation.name, operation.args); } } } export const loadWorld = internalQuery({ args: { worldId: v.id('worlds'), generationNumber: v.number(), }, handler: async (ctx, args) => { return await Game.load(ctx.db, args.worldId, args.generationNumber); }, }); export const saveWorld = internalMutation({ args: { engineId: v.id('engines'), engineUpdate, worldId: v.id('worlds'), worldDiff: gameStateDiff, }, handler: async (ctx, args) => { await applyEngineUpdate(ctx, args.engineId, args.engineUpdate); await Game.saveDiff(ctx, args.worldId, args.worldDiff); }, });