@aige/core
Version:
AI Game Engine
588 lines (584 loc) • 27.1 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const events_1 = __importDefault(require("events"));
const crypto_1 = require("crypto");
const tools_1 = require("./tools");
const client_1 = __importDefault(require("./client"));
const types_1 = require("./types");
/**
* The main class of the library.
* This is the class that you will use to create a game.
*
* @example
* const game = new Game({
* universe: 'Cyberpunk',
* playerName: 'Punk',
* playerClass: 'Hacker'
* })
*/
class Game {
constructor(options = {}) {
this.id = (0, crypto_1.randomUUID)();
this.events = new events_1.default();
this.chats = [];
this.history = [];
this.data = {
armor: 0,
money: 0,
health: 0,
experience: 0,
reputation: 0,
quests: [],
inventory: [],
characters: [],
weight_unit: 'lbs',
weight_capacity: 100,
money_name: 'Credits',
health_description: 'Healthy',
reputation_description: 'Neutral'
};
options.language = options.language || 'en';
options.prompts = options.prompts || {};
options.prompts.create = options.prompts.create || 'Create game';
options.prompts.name = options.prompts.name || 'Get name';
options.prompts.class = options.prompts.class || 'Get class';
options.prompts.summarize = options.prompts.summarize || 'Summarize this';
options.prompts.quest = options.prompts.quest || 'Set the scene and actions to match the next step of this quest';
this.options = options;
this.client = new client_1.default(options.clientOptions, this);
}
/**
* Calculate the player's level based on their experience
*/
get level() {
return Math.round(0.1 * Math.sqrt(this.data.experience)) + 1;
}
/**
* Calculate the player's weight carried
*/
get weightCarried() {
return this.data.inventory?.reduce((total, item) => total + item.weight, 0) || 0;
}
/**
* Determine if the player is overburdened
*/
get overburdened() {
return this.weightCarried > this.data.weight_capacity;
}
/**
* Initialize a new game
*/
async init() {
if (!this.options.universe) {
const universes = (await Promise.resolve().then(() => __importStar(require('./assets/universes')))).default;
this.options.universe = universes[Math.floor(Math.random() * universes.length)];
}
// TODO: Use OpenAI's ability to combine function calls
if (!this.options.playerName)
this.options.playerName = await (0, tools_1.call)(this.client, tools_1.tools.name)({ messages: [{ role: 'user', content: `${this.options.prompts?.name}; universe: ${this.options.universe}, language: ${this.options.language}` }] });
if (!this.options.playerClass)
this.options.playerClass = await (0, tools_1.call)(this.client, tools_1.tools.class)({ messages: [{ role: 'user', content: `${this.options.prompts?.class}; universe: ${this.options.universe}, player: ${this.options.playerName}, language: ${this.options.language}` }] });
const data = await (0, tools_1.call)(this.client, tools_1.tools.create)({
messages: [
{ role: 'user', content: `${this.options.prompts?.create}; universe: ${this.options.universe}, name: ${this.options.playerName}, class: ${this.options.playerClass}, language: ${this.options.language}` }
]
});
this.data = { ...this.data, ...data };
this.events.emit('init', this);
}
/**
* Commit an action on the game
*
* @param action The action to commit
*/
async action(action) {
const questSummary = this.data.quests?.map((quest) => {
const rewardItems = quest.reward?.inventory?.map(item => `${item.name} (${item.type}, ${item.value} ${this.data.money_name})`).join(' | ');
return `${quest.name}: ${quest.reward?.experience} experience, ${quest.reward?.money} ${this.data.money_name}, ${quest.reward?.reputation} reputation, ${rewardItems || ''}`;
});
const dataSummary = `
language: ${this.options.language},
universe: ${this.options.universe},
name: ${this.options.playerName},
class: ${this.options.playerClass},
health: ${this.data.health},
armor: ${this.data.armor},
money: ${this.data.money},
experience: ${this.data.experience},
reputation: ${this.data.reputation},
inventory: ${this.data.inventory?.map(item => item.name).join(', ')},
characters: ${this.data.characters?.map(character => character.name).join(', ')},
quests: ${questSummary},
last action: ${await (0, tools_1.call)(this.client, tools_1.tools.summarize)({ messages: [{ role: 'user', content: `${this.options.prompts?.summarize}: ${this.history[this.history.length - 1]?.content || 'N/A'}` }] })}
last scene: ${await (0, tools_1.call)(this.client, tools_1.tools.summarize)({ messages: [{ role: 'user', content: `${this.options.prompts?.summarize}: ${this.data.scene}` }] })}
`;
const message = { role: 'user', content: `${action}; ${dataSummary}` };
const availableTools = [tools_1.tools.action];
if (['talk', 'chat', 'ask', 'tell', 'speak', 'interact', 'converse', 'dialog', 'dialogue'].some(word => action.toLowerCase().includes(word)))
availableTools.push(tools_1.tools.chat);
const data = await (0, tools_1.call)(this.client, availableTools)({ messages: [message] });
if (!data)
throw new Error('No data returned');
this.history.push(message);
if (!this.data.characters)
this.data.characters = [];
if (!this.data.inventory)
this.data.inventory = [];
if (!this.data.quests)
this.data.quests = [];
if (!this.chats)
this.chats = [];
if (data.chat) {
const chat = this.chats.find(chat => chat.character_name === data.name);
const character = this.data.characters.find(character => character.name === data.name);
this.events.emit('chat', { chat, character, dialog: data.dialog, game: this });
if (!chat) {
const chat = {
character_name: data.name,
messages: [{
from_player: false,
content: data.dialog,
timestamp: new Date().toISOString()
}]
};
this.chats.push(chat);
}
else {
chat.messages.push({
from_player: false,
content: data.dialog,
timestamp: new Date().toISOString()
});
}
if (data.effects) {
data.health_delta = data.effects.health_delta;
data.armor_delta = data.effects.armor_delta;
data.money_delta = data.effects.money_delta;
data.experience_delta = data.effects.experience_delta;
data.reputation_delta = data.effects.reputation_delta;
data.inventory_added = data.effects.inventory_added;
data.inventory_removed = data.effects.inventory_removed;
data.quest_added = data.effects.quest_added;
data.quest_removed = data.effects.quest_removed;
}
}
if (data.health_delta)
this.data.health += data.health_delta;
if (data.armor_delta)
this.data.armor += data.armor_delta;
if (data.money_delta)
this.data.money += data.money_delta;
if (data.experience_delta)
this.data.experience += data.experience_delta;
if (this.data.health < 0)
this.data.health = 0;
if (this.data.armor < 0)
this.data.armor = 0;
if (this.data.money < 0)
this.data.money = 0;
if (this.data.health === 0)
this.events.emit(types_1.GameEvent.death, { game: this });
if (this.data.armor === 0)
this.events.emit(types_1.GameEvent.armor_destroyed, { game: this });
if (this.data.money === 0)
this.events.emit(types_1.GameEvent.financial_ruin, { game: this });
if (data.reputation_delta) {
this.data.reputation += data.reputation_delta;
const description = await (0, tools_1.call)(this.client, tools_1.tools.reputation)({
messages: [
{ role: 'user', content: `Universe: ${this.options.universe}, Player: ${this.options.playerName}` },
{ role: 'user', content: `Get player perspective of reputation description; new reputation: ${this.data.reputation}` }
]
});
this.data.reputation_description = description.short_description || description.long_description;
}
if (data.inventory_added) {
this.events.emit(types_1.GameEvent.inventory_added, { item: data.inventory_added, game: this });
this.data.inventory.push(data.inventory_added);
}
if (data.inventory_removed) {
const item = this.data.inventory.find(item => item.name === data.inventory_removed);
if (item)
this.events.emit(types_1.GameEvent.inventory_removed, { item, game: this });
this.data.inventory = this.data.inventory.filter(item => data.inventory_removed !== item.name);
}
if (data.character_added) {
if (data.character_added.name !== this.options.playerName) {
this.events.emit(types_1.GameEvent.character_added, { character: data.character_added, game: this });
this.data.characters.push(data.character_added);
}
}
if (data.character_removed) {
const character = this.data.characters.find(character => character.name === data.character_removed);
if (character)
this.events.emit(types_1.GameEvent.character_removed, { character, game: this });
this.data.characters = this.data.characters.filter(character => data.character_removed !== character.name);
}
if (data.quest_added) {
this.events.emit(types_1.GameEvent.quest_added, { quest: data.quest_added, game: this });
this.data.quests.push(data.quest_added);
}
if (data.quest_removed) {
const quest = this.data.quests.find(quest => quest.name === data.quest_removed);
if (quest) {
if (quest.reward?.experience)
this.data.experience += quest.reward.experience;
if (quest.reward?.money)
this.data.money += quest.reward.money;
if (quest.reward?.reputation)
this.data.reputation += quest.reward.reputation;
if (quest.reward?.inventory)
this.data.inventory.push(...quest.reward.inventory);
this.events.emit(types_1.GameEvent.quest_removed, { quest, game: this });
this.data.quests = this.data.quests.filter(quest => quest.name !== data.quest_removed);
}
}
if (this.data.health <= 0)
throw new Error('You died');
for (const delta of ['health_delta', 'armor_delta', 'money_delta', 'experience_delta', 'reputation_delta']) {
if (!data[delta])
continue;
const attribute = delta.split('_')[0];
if (data[delta] > 0)
this.events.emit(types_1.GameEvent.gain, { attribute, amount: data[delta] });
else
this.events.emit(types_1.GameEvent.loss, { attribute, amount: data[delta] });
}
delete data.health_delta;
delete data.armor_delta;
delete data.money_delta;
delete data.experience_delta;
delete data.inventory_added;
delete data.inventory_removed;
delete data.quest_added;
delete data.quest_removed;
this.data = { ...this.data, ...data };
this.events.emit(types_1.GameEvent.action, { action, game: this });
return this;
}
/**
* Send a chat message to a character
*
* Listen for game.events.on(GameEvent.chat) to get the response
*
* @todo Prune chat history to fit context window
*/
async chat(data) {
let chat = this.chats.find(chat => chat.character_name === data.character.name);
if (!chat) {
chat = {
character_name: data.character.name,
messages: [{
from_player: true,
content: data.dialog,
timestamp: new Date().toISOString()
}]
};
this.chats.push(chat);
}
else {
chat.messages.push({
from_player: true,
content: data.dialog,
timestamp: new Date().toISOString()
});
}
const messages = [
{
role: 'user',
content: `
Game Info:
Universe: ${this.options.universe}
Location: ${this.data.location} (${this.data.location_description})
Weather: ${this.data.weather} (${this.data.weather_description})
Rumor: ${this.data.rumor}
Characters: ${this.data.characters?.map(character => character.name).join(', ') || 'None'}
Scene (from player perspective): ${this.data.scene}
I am the character, ${data.character.name}, with these stats:
Health: ${data.character.health}
Armor: ${data.character.armor}
Money: ${data.character.money}
Reputation: ${data.character.reputation}
Inventory: ${data.character.inventory?.map(item => item.name).join(', ') || 'None'}
Abilities: ${data.character.abilities?.map(ability => `${ability.name} (${ability.description})`).join(' | ') || 'None'}
I am ${data.character.hostile ? 'hostile' : 'friendly'} to the player
I am ${data.character.alive ? 'alive' : 'dead'}
I am speaking to the player, ${this.options.playerName}, class of ${this.options.playerClass}, with these stats:
Armor: ${this.data.armor}%
Money: ${this.data.money}
Health (from player perspective): ${this.data.health}
Reputation (from player perspective): ${this.data.reputation} (${this.data.reputation_description})
Appearance (from player perspective): ${this.data.appearance}
I can't forget, I'm ${data.character.name} speaking to ${this.options.playerName} - I must not break character!
`
},
...chat.messages.map(message => ({ role: message.from_player ? 'user' : 'assistant', content: message.content })),
];
const tool_choice = { type: 'function', function: { name: 'chat' } };
const response = await (0, tools_1.call)(this.client, tools_1.tools.chat)({ tool_choice, messages });
if (!response)
throw new Error('No response found');
const { dialog } = response;
this.events.emit('chat', { chat, dialog, character: data.character, game: this });
chat.messages.push({ from_player: false, content: dialog, timestamp: new Date().toISOString() });
// TODO: Merge this with the action function's effect processing
if (response.effects) {
if (!this.data.quests)
this.data.quests = [];
if (!this.data.inventory)
this.data.inventory = [];
if (!this.data.characters)
this.data.characters = [];
if (response.effects.health_delta)
this.data.health += response.effects.health_delta;
if (response.effects.armor_delta)
this.data.armor += response.effects.armor_delta;
if (response.effects.money_delta)
this.data.money += response.effects.money_delta;
if (response.effects.experience_delta)
this.data.experience += response.effects.experience_delta;
if (response.effects.reputation_delta)
this.data.reputation += response.effects.reputation_delta;
if (response.effects.inventory_added)
this.data.inventory.push(response.effects.inventory_added);
if (response.effects.inventory_removed)
this.data.inventory = this.data.inventory.filter(item => response.effects.inventory_removed !== item.name);
if (response.effects.quest_added)
this.data.quests.push(response.effects.quest_added);
if (response.effects.quest_removed)
this.data.quests = this.data.quests.filter(quest => response.effects.quest_removed !== quest.name);
if (this.data.health < 0)
this.data.health = 0;
if (this.data.armor < 0)
this.data.armor = 0;
if (this.data.money < 0)
this.data.money = 0;
if (this.data.health === 0)
this.events.emit(types_1.GameEvent.death, { game: this });
if (this.data.armor === 0)
this.events.emit(types_1.GameEvent.armor_destroyed, { game: this });
if (this.data.money === 0)
this.events.emit(types_1.GameEvent.financial_ruin, { game: this });
if (response.effects.inventory_added)
this.events.emit(types_1.GameEvent.inventory_added, { item: response.effects.inventory_added, game: this });
if (response.effects.inventory_removed) {
const item = this.data.inventory.find(item => item.name === response.effects.inventory_removed);
if (item)
this.events.emit(types_1.GameEvent.inventory_removed, { item, game: this });
}
if (response.effects.quest_added)
this.events.emit(types_1.GameEvent.quest_added, { quest: response.effects.quest_added, game: this });
if (response.effects.quest_removed) {
const quest = this.data.quests.find(quest => quest.name === response.effects.quest_removed);
if (quest)
this.events.emit(types_1.GameEvent.quest_removed, { quest, game: this });
}
if (response.effects.character_added)
this.events.emit(types_1.GameEvent.character_added, { character: response.effects.character_added, game: this });
if (response.effects.character_removed) {
const character = this.data.characters.find(character => character.name === response.effects.character_removed);
if (character)
this.events.emit(types_1.GameEvent.character_removed, { character, game: this });
}
if (response.effects.reputation_delta) {
const description = await (0, tools_1.call)(this.client, tools_1.tools.reputation)({
messages: [
{ role: 'user', content: `Universe: ${this.options.universe}, Player: ${this.options.playerName}` },
{ role: 'user', content: `Get player perspective of reputation description; new reputation: ${this.data.reputation}` }
]
});
this.data.reputation_description = description.short_description || description.long_description;
}
for (const delta of ['health_delta', 'armor_delta', 'money_delta', 'experience_delta', 'reputation_delta']) {
if (!response.effects[delta])
continue;
const attribute = delta.split('_')[0];
if (response.effects[delta] > 0)
this.events.emit(`gain`, { attribute, amount: response.effects[delta] });
else
this.events.emit(`loss`, { attribute, amount: response.effects[delta] });
}
}
}
/**
* Create images of various game objects
*
* Call game.client.image to manually use any prompt
*/
async images({ cover, scene, player, character, item, ability } = {}, imageOptions) {
const tasks = [];
const results = {};
if (cover) {
const prompt = `
A highly detailed landscape cover photo of ${this.options.playerName},
a ${this.options.playerClass} in the ${this.options.universe} universe.
Drawn in the style of ${this.options.universe} if possible.
`;
tasks.push(this.client.image(prompt, imageOptions?.cover));
}
if (scene) {
const prompt = `
A highly detailed photo of the current scene in the ${this.options.universe} universe.
Scene: ${this.data.scene}.
Location: ${this.data.location}.
Weather: ${this.data.weather}.
Drawn in the style of ${this.options.universe}
`;
tasks.push(this.client.image(prompt, imageOptions?.scene));
}
if (player) {
const prompt = `
A highly detailed photo of ${this.options.playerName}, a ${this.options.playerClass === '' ? 'character' : this.options.playerClass} in the ${this.options.universe} universe.
Appearance: ${this.data.appearance}.
Health: ${this.data.health}.
Weather: ${this.data.weather}.
Scene: ${this.data.scene}.
Location: ${this.data.location}.
Drawn in the style of ${this.options.universe}
`;
tasks.push(this.client.image(prompt, imageOptions?.player));
}
if (character) {
const prompt = `
A highly detailed photo of ${character.name}, a character in the ${this.options.universe} universe.
Appearance: ${character.appearance}.
Health: ${character.health}.
Weather: ${this.data.weather}.
Location: ${this.data.location}.
Drawn in the style of ${this.options.universe}
`;
tasks.push(this.client.image(prompt, imageOptions?.character));
}
if (item) {
const prompt = `
A highly detailed photo of ${item.name}, an item in the ${this.options.universe} universe.
It is a ${item.type} worth ${item.value} ${this.data.money_name}, weighing ${item.weight} ${this.data.weight_unit}.
Drawn in the style of ${this.options.universe}
`;
tasks.push(this.client.image(prompt, imageOptions?.item));
}
if (ability) {
const prompt = `
A highly detailed photo of ${this.options.playerName} using ${ability.name}, an ability in the ${this.options.universe} universe.
Drawn in the style of ${this.options.universe}
`;
tasks.push(this.client.image(prompt, imageOptions?.ability));
}
const images = await Promise.all(tasks);
if (cover)
results.cover = images.shift();
if (scene)
results.scene = images.shift();
if (player)
results.player = images.shift();
if (character)
results.character = images.shift();
if (item)
results.item = images.shift();
if (ability)
results.ability = images.shift();
this.events.emit(types_1.GameEvent.images_created, { images: results, game: this });
return results;
}
/**
* Inspect the game data
*/
inspect() {
const tokens = Intl.NumberFormat().format(this.client.tokens);
return `Game([${this.id}]\n${JSON.stringify({ ...this.options, ...this.data }, null, 2)}): ${tokens} tokens used`;
}
/**
* Resolve a path in the game data
*/
resolvePath(obj, path) {
// Split the path into keys, handling both dot and bracket notation
const keys = path.replace(/\[(\w+)\]/g, '.$1').split('.');
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (current[key] === undefined)
throw new Error(`Path not found: ${keys.slice(0, i + 1).join('.')}`);
current = current[key];
}
const lastKey = keys[keys.length - 1];
return {
get value() {
return current[lastKey];
},
set value(newValue) {
current[lastKey] = newValue;
}
};
}
/**
* Manually set a value in the game data
*/
set(key, value) {
const reference = this.resolvePath(this.data, key);
reference.value = value;
}
/**
* Listen for game events
*/
on(event, listener) {
this.events.addListener(event, listener);
}
/**
* Stop listening for game events
*/
off(event, listener) {
this.events.removeListener(event, listener);
}
/**
* Export the game data object
*/
export() {
return {
id: this.id,
tokens: this.client.tokens,
options: this.options,
data: this.data,
chats: this.chats,
history: this.history
};
}
/**
* Import a game data object
*/
import(data) {
this.id = data.id;
this.options = data.options;
this.data = data.data;
this.chats = data.chats;
this.history = data.history;
this.client.tokens = data.tokens;
this.events.emit(types_1.GameEvent.import, this);
}
}
exports.default = Game;