Jofthomas's picture
Jofthomas HF staff
bulk morning
df2ef4f
raw
history blame
14.9 kB
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<typeof gameState>;
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<typeof gameStateDiff>;
export class Game extends AbstractGame {
tickDuration = 16;
stepDuration = 1000;
maxTicksPerStep = 600;
maxInputsPerStep = 32;
world: World;
historicalLocations: Map<GameId<'players'>, HistoricalObject<Location>>;
descriptionsModified: boolean;
worldMap: WorldMap;
playerDescriptions: Map<GameId<'players'>, PlayerDescription>;
agentDescriptions: Map<GameId<'agents'>, 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<T extends IdTypes>(idType: T): GameId<T> {
const id = allocGameId(idType, this.world.nextId);
this.world.nextId += 1;
return id;
}
scheduleOperation(name: string, args: unknown) {
this.pendingOperations.push({ name, args });
}
handleInput<Name extends InputNames>(now: number, name: Name, args: InputArgs<Name>) {
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<void> {
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);
},
});