Dynamic Commentary with Local Language Models
Introduction
Browser games traditionally follow predictable patterns, fixed text messages, static tutorials, scripted NPC responses. Players see the same "Game Over" message whether they nearly won or failed spectacularly. Tutorial text remains identical regardless of player skill level. The game experience, while fun, lacks the dynamic reactivity of human-moderated gameplay. What if your Space Invaders game could comment on gameplay in real-time? Taunt players when they miss easy shots? Celebrate close victories with personalized messages? Adjust difficulty suggestions based on actual performance metrics? This article demonstrates exactly that: integrating AI-powered dynamic commentary into a browser game using Spaceinvaders-FoundryLocal, vanilla JavaScript, and Microsoft Foundry Local.
You'll learn how to integrate local AI into client-side games, design AI personality systems that enhance rather than distract, implement context-aware commentary generation, and architect optional AI features that don't break core gameplay when unavailable. Whether you're building educational games, interactive training simulations, or simply adding personality to entertainment projects, this approach provides a blueprint for AI-enhanced gaming experiences.
Why Local AI Transforms Browser Gaming
Adding AI to games sounds expensive, cloud API costs scale with player counts, introducing per-gameplay pricing that makes free-to-play models challenging. Privacy concerns emerge when gameplay data leaves user devices. Latency affects real-time experiences, waiting 2 seconds for commentary after an action breaks immersion. Network requirements exclude offline play.
Local AI solves all these challenges simultaneously. Foundry Local runs Small Language Models (SLMs) entirely on player devices, no API costs, no data leaving the machine, no network dependency. Inference happens in milliseconds, enabling truly real-time responses. Games work offline after initial load, perfect for mobile or low-connectivity scenarios.
SLMs excel at personality-driven tasks like game commentary. They don't need perfect factual recall or complex reasoning, they generate entertaining, contextually relevant text based on game state. A 1.5B parameter model produces engaging taunts and celebration messages indistinguishable from hand-written content, while running easily on mid-range laptops. Integrating AI as an optional enhancement demonstrates good architecture. Core gameplay must function perfectly without AI, commentary enhances the experience but failure doesn't break the game. This graceful degradation pattern ensures maximum compatibility while offering AI features to capable devices.
Architecture: Progressive Enhancement with AI
The Spaceinvaders-FoundryLocal implementation uses progressive enhancement, the game fully works without AI, but adds dynamic personality when available:
The base game implements classic Space Invaders mechanics entirely in vanilla JavaScript. Player ship movement, bullet physics, enemy patterns, collision detection, scoring, and power-up systems all operate independently of AI. This ensures universal compatibility across browsers, devices, and network conditions.
The AI layer adds dynamic commentary through a backend Node.js proxy. The proxy runs locally, communicates with Foundry Local, and provides game context to the AI for generating personalized messages. The game polls the proxy periodically, sending current game state (score, accuracy, wave number, power-up usage) and receiving commentary responses.
The architecture flow for AI-enhanced gameplay:
Player Action (e.g., destroys enemy)
↓
Game Updates State (score += 100, accuracy tracked)
↓
Game Checks AI Status (polling every 5 seconds)
↓
If AI Available:
Send Game Context to Backend → {
event: 'wave_complete',
score: 2500,
accuracy: 78%,
wave: 3
}
↓
Backend builds prompt with context
↓
Foundry Local generates comment
↓
Return commentary to game → "Wave 3 conquered! Your 78% accuracy shows improving skills."
↓
Display in game UI (animated text bubble)
This design demonstrates several key patterns:
- Zero-dependency core: Game playable immediately, AI adds value incrementally
- Graceful degradation: If AI unavailable, game shows generic messages
- Asynchronous enhancement: AI runs in background, never blocks gameplay
- Context-aware generation: Commentary reflects actual player performance
- Local-first architecture: Everything runs on player's machine—no servers, no tracking
Implementing Context-Aware AI Commentary
Effective game commentary requires understanding current gameplay context. The AI needs to know what just happened, how the player is performing, and what makes this moment interesting:
// llm.js - AI integration module
export class GameAI {
constructor() {
this.baseURL = 'http://localhost:3001'; // Local proxy server
this.available = false;
this.checkAvailability();
}
async checkAvailability() {
try {
const response = await fetch(`${this.baseURL}/health`, {
method: 'GET',
timeout: 2000
});
this.available = response.ok;
return this.available;
} catch (error) {
console.log('AI server not available (optional feature)');
this.available = false;
return false;
}
}
async generateComment(gameContext) {
if (!this.available) {
return this.getFallbackComment(gameContext.event);
}
try {
const response = await fetch(`${this.baseURL}/api/comment`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(gameContext)
});
if (!response.ok) {
throw new Error('AI request failed');
}
const data = await response.json();
return data.comment;
} catch (error) {
console.error('AI comment generation failed:', error);
return this.getFallbackComment(gameContext.event);
}
}
getFallbackComment(event) {
// Static messages when AI unavailable
const fallbacks = {
'wave_complete': 'Wave cleared!',
'player_hit': 'Shields damaged!',
'game_over': 'Game Over. Try again!',
'high_score': 'New high score!',
'power_up': 'Power-up collected!'
};
return fallbacks[event] || 'Good job!';
}
}
The backend processes game context and generates contextually relevant commentary:
// server.js - Node.js backend proxy
import express from 'express';
import { FoundryLocalClient } from 'foundry-local-sdk';
const app = express();
const foundry = new FoundryLocalClient({
endpoint: process.env.FOUNDRY_LOCAL_ENDPOINT || 'http://127.0.0.1:5272'
});
app.use(express.json());
app.use(express.cors()); // Allow browser game to connect
app.get('/health', (req, res) => {
res.json({ status: 'AI available', model: 'phi-3.5-mini' });
});
app.post('/api/comment', async (req, res) => {
const { event, score, accuracy, wave, lives, combo } = req.body;
// Build context-rich prompt
const prompt = buildCommentPrompt(event, {
score,
accuracy,
wave,
lives,
combo
});
try {
const completion = await foundry.chat.completions.create({
model: 'phi-3.5-mini',
messages: [
{
role: 'system',
content: `You are an AI commander providing brief, encouraging commentary for a Space Invaders game. Be energetic, supportive, and sometimes humorous. Keep responses to 1-2 sentences maximum. Reference specific game metrics when relevant.`
},
{
role: 'user',
content: prompt
}
],
temperature: 0.9, // High temperature for creative variety
max_tokens: 50
});
const comment = completion.choices[0].message.content.trim();
res.json({
comment,
model: 'phi-3.5-mini',
timestamp: new Date().toISOString()
});
} catch (error) {
console.error('AI generation error:', error);
res.status(500).json({ error: 'Commentary generation failed' });
}
});
function buildCommentPrompt(event, context) {
switch(event) {
case 'wave_complete':
return `The player just completed wave ${context.wave} with score ${context.score}. Their shooting accuracy is ${context.accuracy}%. ${context.lives} lives remaining. Generate an encouraging comment about their progress.`;
case 'player_hit':
return `The player got hit by an enemy! They now have ${context.lives} lives left. Score: ${context.score}. Provide a brief motivational comment to keep them engaged.`;
case 'game_over':
if (context.accuracy > 70) {
return `Game over at wave ${context.wave}, score ${context.score}. The player had ${context.accuracy}% accuracy - pretty good! Generate an encouraging comment acknowledging their skill.`;
} else {
return `Game over at wave ${context.wave}, score ${context.score}. Accuracy was ${context.accuracy}%. Provide a supportive comment with a tip for improvement.`;
}
case 'combo_streak':
return `Player achieved a ${context.combo}x combo streak! Score: ${context.score}. Generate an excited celebration comment.`;
case 'power_up_used':
return `Player activated a ${context.power_up_type} power-up. Generate a brief tactical comment about using it effectively.`;
default:
return `General gameplay comment. Score: ${context.score}, Wave: ${context.wave}.`;
}
}
const PORT = 3001;
app.listen(PORT, () => {
console.log(`✓ Game AI server running on http://localhost:${PORT}`);
console.log(`✓ Foundry Local endpoint: ${process.env.FOUNDRY_LOCAL_ENDPOINT || 'http://127.0.0.1:5272'}`);
});
This backend demonstrates several best practices:
- Context-sensitive prompting: Different events get different prompt templates with relevant metrics
- Personality consistency: System message establishes tone and style guidelines
- Brevity constraints:
max_tokens: 50ensures comments don't overwhelm UI - Creative variety: High temperature (0.9) produces diverse commentary on repeated events
- Performance-aware feedback: Comments adapt based on accuracy, lives remaining, combo streaks
Integrating AI into Game Loop Without Performance Impact
Games require 60 FPS to feel smooth, any blocking operation creates stutter. AI integration must be completely asynchronous and non-blocking:
// game.js - Main game loop
class SpaceInvadersGame {
constructor() {
this.ai = new GameAI();
this.lastAIUpdate = 0;
this.aiUpdateInterval = 5000; // Poll AI every 5 seconds
this.pendingAIRequest = false;
// ... other game state
}
update(deltaTime) {
// Core game logic (always runs)
this.updatePlayer(deltaTime);
this.updateEnemies(deltaTime);
this.updateBullets(deltaTime);
this.checkCollisions();
this.updatePowerUps(deltaTime);
// AI commentary (optional, async)
this.updateAI(deltaTime);
}
updateAI(deltaTime) {
this.lastAIUpdate += deltaTime;
// Only check AI periodically, never block gameplay
if (this.lastAIUpdate >= this.aiUpdateInterval && !this.pendingAIRequest) {
this.requestAICommentary();
}
}
async requestAICommentary() {
// Check if there's an interesting event to comment on
const event = this.getSignificantEvent();
if (!event) return;
this.pendingAIRequest = true;
// Fire-and-forget async request
this.ai.generateComment({
event: event.type,
score: this.score,
accuracy: this.calculateAccuracy(),
wave: this.currentWave,
lives: this.lives,
combo: this.comboMultiplier
})
.then(comment => {
this.displayAIComment(comment);
this.lastAIUpdate = 0;
})
.catch(error => {
console.log('AI comment failed (non-critical):', error);
})
.finally(() => {
this.pendingAIRequest = false;
});
}
getSignificantEvent() {
// Determine what's worth commenting on
if (this.justCompletedWave) {
this.justCompletedWave = false;
return { type: 'wave_complete' };
}
if (this.justGotHit) {
this.justGotHit = false;
return { type: 'player_hit' };
}
if (this.comboMultiplier >= 5) {
return { type: 'combo_streak' };
}
return null; // Nothing interesting right now
}
displayAIComment(comment) {
// Show comment in animated text bubble
const bubble = document.createElement('div');
bubble.className = 'ai-comment-bubble';
bubble.textContent = comment;
document.getElementById('game-container').appendChild(bubble);
// Animate in
setTimeout(() => bubble.classList.add('show'), 50);
// Remove after 4 seconds
setTimeout(() => {
bubble.classList.remove('show');
setTimeout(() => bubble.remove(), 500);
}, 4000);
}
calculateAccuracy() {
if (this.shotsFired === 0) return 0;
return Math.round((this.shotsHit / this.shotsFired) * 100);
}
}
This integration pattern ensures:
- Zero gameplay impact: AI runs completely asynchronously—game never waits for AI
- Periodic updates only: Check AI every 5 seconds, not every frame (60 FPS → minimal CPU overhead)
- Event-driven commentary: Only request comments for significant moments, not continuous chatter
- Non-blocking display: Comments appear as animated overlays that don't interrupt gameplay
- Graceful failure: AI errors logged but never shown to players—game continues normally
Designing AI Personality Systems
Effective game AI has consistent personality that enhances rather than distracts. The system message establishes tone, response templates ensure variety, and context awareness makes commentary relevant:
// Enhanced system message for consistent personality
const AI_COMMANDER_PERSONALITY = `
You are AEGIS, an AI defense commander providing tactical commentary for a Space Invaders-style game. Your personality traits:
- Enthusiastic but professional military commander tone
- Celebrate victories with tactical language ("Excellent flanking maneuver!")
- Acknowledge defeats with constructive feedback ("Regroup and maintain formation!")
- Reference specific metrics to show you're paying attention
- Keep responses to 1-2 sentences maximum
- Use occasional humor but stay in character
- Be encouraging even when player struggles
Examples of your style:
- "Wave neutralized! Your 85% accuracy shows precision targeting."
- "Shield integrity compromised! Fall back and reassess the battlefield."
- "Impressive combo multiplier! Sustained fire superiority achieved."
- "That power-up spread pattern cleared the sector perfectly."
`;
// Context-aware response variety
const RESPONSE_TEMPLATES = {
wave_complete: {
high_performance: [
"Your {accuracy}% accuracy led to decisive victory, Commander!",
"Wave {wave} eliminated with tactical excellence!",
"Strategic brilliance! {accuracy}% hit rate maintained."
],
medium_performance: [
"Wave {wave} cleared. Solid tactics, Commander.",
"Sector secured. Your {accuracy}% accuracy shows improvement potential.",
"Objective achieved. Recommend tightening shot discipline."
],
low_performance: [
"Wave {wave} cleared, but {accuracy}% accuracy needs work.",
"Victory secured. Focus on accuracy in next engagement.",
"Mission accomplished, though your hit rate needs improvement."
]
},
player_hit: {
lives_critical: [
"Critical damage! Only {lives} lives remain - exercise extreme caution!",
"Shields failing! {lives} backup systems active.",
"Red alert! Hull integrity at {lives} units."
],
lives_okay: [
"Shields damaged. {lives} lives remaining. Stay focused!",
"Hit sustained. {lives} backup systems online.",
"Damage taken. Maintain defensive posture."
]
}
};
function selectResponseTemplate(event, context) {
const templates = RESPONSE_TEMPLATES[event];
if (!templates) return;
// Choose template category based on context
let category;
if (event === 'wave_complete') {
if (context.accuracy >= 75) category = templates.high_performance;
else if (context.accuracy >= 50) category = templates.medium_performance;
else category = templates.low_performance;
} else if (event === 'player_hit') {
category = context.lives <= 2
? templates.lives_critical
: templates.lives_okay;
}
// Randomly select from category for variety
const template = category[Math.floor(Math.random() * category.length)];
// Fill in context variables
return template
.replace('{accuracy}', context.accuracy)
.replace('{wave}', context.wave)
.replace('{lives}', context.lives);
}
This personality system creates:
- Consistent character: AEGIS always sounds like a military commander, never breaks character
- Context-appropriate responses: Different situations trigger different tones (celebration vs concern)
- Natural variety: Template randomization prevents repetitive commentary
- Metric awareness: Specific references to accuracy, lives, waves show AI is "watching"
- Encouraging feedback: Even in failure scenarios, provides constructive guidance
Key Takeaways and Game AI Design Patterns
Integrating AI into browser games demonstrates that advanced features don't require cloud services or complex infrastructure. Local AI enables personality-driven enhancements that run entirely on player devices, cost nothing at scale, and work offline.
Essential principles for game AI integration:
- Progressive enhancement architecture: Core gameplay must work perfectly without AI—commentary enhances but isn't required
- Asynchronous-only integration: Never block game loop for AI—60 FPS gameplay is non-negotiable
- Context-aware generation: Commentary reflecting actual game state feels intelligent, generic messages feel robotic
- Personality consistency: Well-defined character voice creates memorable experiences
- Graceful failure handling: AI errors should be invisible to players—fallback to static messages
- Performance-conscious polling: Check AI every few seconds, not every frame
- Event-driven commentary: Only generate responses for significant moments
This pattern extends beyond games, any interactive application benefits from context-aware AI personality: educational software providing personalized encouragement, fitness apps offering adaptive coaching, productivity tools giving motivational feedback.
The complete implementation with game engine, AI integration, backend proxy, and deployment instructions is available at github.com/leestott/Spaceinvaders-FoundryLocal. Clone the repository to experience AI-enhanced gaming—just open index.html and start playing immediately, then optionally enable AI features for dynamic commentary.
Resources and Further Reading
- Space Invaders with AI Repository - Complete game with AI integration
- Quick Start Guide - Play immediately or enable AI features
- Microsoft Foundry Local Documentation - SDK and model reference
- MDN Game Development - Browser game development patterns
- HTML5 Game Devs Forum - Community discussions and techniques