mini-world / aiService.ts
victor's picture
victor HF Staff
Initial deployment of Mini World game
a2d0320 verified
import { OpenAI } from "openai";
// Configuration - use specified model with fallback to env vars
export const MODEL =
process.env.HF_MODEL ||
process.env.OPENAI_MODEL ||
"zai-org/GLM-4.7-Flash:novita";
const HF_TOKEN = process.env.HF_TOKEN;
if (!HF_TOKEN) {
throw new Error(
"HF_TOKEN environment variable is required for Hugging Face Router authentication."
);
}
const SYS_PROMPT = `
# Welcome to the you own exploration game
You are acting as an explorer in a 2D world game seen from a top-down perspective. Everything in this world, including objects and landmarks, is represented by emoji.
The most important emoji is ๐Ÿšถ (it's you).
## Important concepts
### Turn-based
You can take one action per turn, then you'll get the result of your action before you can take another action.
### Goals
Your primary goal is to collect as many diamonds (๐Ÿ’Ž) as possible. A good strategy is to explore efficiently and avoid revisiting the same tiles while searching for diamonds.
## Available Actions
You must respond your next action in JSON format with what action you want to take. Give no explanations just return the JSON.
### Move
This action allows you to move one emoji away from your current location, choosing between the four available direct tiles around you (๐Ÿšถ): "up", "right", "down", or "left".
You are only allowed to move on the empty โฌœ square, so always make sure that your target square is a โฌœ.
{ "action": "move", "detail": move direction, can be "up", "right", "down", or "left" }
### Pick
This action allows you to pick up an object from an adjacent tile (up, right, down, or left).
The object will be added to your inventory and removed from the map.
{ "action": "pick", "detail": direction of the object to pick, can be "up", "right", "down", or "left" }`;
// OpenAI client configured for HuggingFace router
const client = new OpenAI({
baseURL: "https://router.huggingface.co/v1",
apiKey: HF_TOKEN,
});
export class AIService {
/**
* Get the next action from the AI agent
* Returns a validated action object { action: "move"|"pick", detail: "up"|"down"|"left"|"right" }
*/
static async getNextAction(
map: string,
history: any[]
): Promise<{ action: string; detail: string }> {
const messages: OpenAI.ChatCompletionMessageParam[] = [
{
role: "system",
content: SYS_PROMPT,
},
...history.slice(-15),
{
role: "user",
content: `Please decide on what action to do next and provide the corresponding JSON response. Here is the current map:
${map}
`,
},
];
// Log messages for debugging
Bun.write("messages.json", JSON.stringify(messages, null, 2));
try {
const chatCompletion = await client.chat.completions.create({
model: MODEL,
messages,
temperature: 0.7,
});
let content = chatCompletion.choices[0].message.content?.trim() ?? "";
console.log("Raw AI response:", content);
// Strip markdown code blocks if present
if (content.startsWith("```")) {
content = content.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
}
// Parse and validate the response
const parsed = JSON.parse(content);
if (this.isValidAction(parsed)) {
return {
action: parsed.action,
detail: parsed.detail.toLowerCase(),
};
}
throw new Error("Invalid action format");
} catch (error) {
console.error("Error getting AI action:", error);
// Return a safe fallback action if anything fails
return { action: "move", detail: "right" };
}
}
private static isValidAction(obj: any): boolean {
if (!obj || typeof obj !== "object" || typeof obj.action !== "string") {
return false;
}
if (obj.action === "move" || obj.action === "pick") {
return (
typeof obj.detail === "string" &&
["up", "down", "left", "right"].includes(obj.detail.toLowerCase())
);
}
return false;
}
}