In the village square, two merchants are locked in a ruthless battle for bread supremacy. But these aren't human players—they're AI agents powered by Google Gemini and Qwen 2.5.
In this post, we'll explore how we built AI Price War, a real-time market simulation that combines turn-based AI decision-making with a high-performance canvas-based customer simulation.
The Vision
The goal was to create a "living" market where:
- AI Agents act as shop owners, making strategic decisions on pricing and marketing.
- A Simulation models individual customer behavior, reacting to those AI decisions.
- A Critic AI evaluates the "hype" of marketing slogans to influence customer choices.
The Architecture
The application is a React 19 SPA, but it's more than just a dashboard. It's a hybrid of a turn-based game and a continuous physics simulation.
1. The AI Decision Loop (runTurn)
Every 10 seconds, the runTurn function orchestrates the AI's moves. It gathers the current market state (competitor prices, balance, reputation, and sentiment) and sends it to the agents.
// src/App.tsx
const runTurn = async () => {
// 1. Get moves from both agents in parallel
const [moveA, moveB] = await Promise.all([
getGeminiMove(stateA, geminiKey),
getFeatherlessMove(stateB, featherlessKey)
]);
// 2. Evaluate slogans with a third "Critic" AI
const [hypeA, hypeB] = await Promise.all([
getMarketCriticScore(moveA.ad_slogan),
getMarketCriticScore(moveB.ad_slogan)
]);
// 3. Update the global simulation state
updateSimulationState(moveA, moveB, hypeA, hypeB);
};
2. The AI Agents (src/lib/ai-agents.ts)
We used a multi-model approach. Shop A uses Gemini via the @google/genai SDK, while Shop B uses Qwen 2.5 via the Featherless.ai API (using the OpenAI client).
A key challenge was ensuring the AI returned structured data. We used JSON Schema with Gemini and response_format: { type: "json_object" } with Qwen to guarantee valid JSON outputs.
// Example: Gemini Move Request
const response = await ai.models.generateContent({
model: "gemini-3-flash-preview",
contents: prompt,
config: {
systemInstruction: SYSTEM_PROMPT,
responseMimeType: "application/json",
responseSchema: {
type: Type.OBJECT,
properties: {
price: { type: Type.NUMBER },
ad_slogan: { type: Type.STRING },
strategy_reasoning: { type: Type.STRING }
},
required: ["price", "ad_slogan", "strategy_reasoning"]
}
}
});
3. The Canvas Simulation (VillageSquare.tsx)
To make the market feel alive, we built a customer simulation using the HTML5 Canvas API. Each customer is an object with its own priceSensitivity and hypeAffinity.
To avoid the "update during render" React error, we moved the high-frequency simulation data into a useRef and used requestAnimationFrame for the update loop.
// src/components/VillageSquare.tsx
const update = () => {
customersRef.current = customersRef.current.map(c => {
const targetPos = c.target === 'A' ? shopA_Pos : shopB_Pos;
const dx = targetPos.x - c.pos.x;
const dy = targetPos.y - c.pos.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 10) {
onSale(c.target); // Side effect: trigger a sale in the parent state
return { ...c, reached: true };
}
return {
...c,
pos: {
x: c.pos.x + (dx / dist) * c.speed,
y: c.pos.y + (dy / dist) * c.speed
}
};
}).filter(c => !c.reached);
forceUpdate({}); // Trigger a re-render to draw the new frame
requestAnimationFrame(update);
};
Dynamic Market Sentiment
One of the most interesting features is the Dynamic Market Sentiment. The village's mood isn't static; it's a reflection of the AI's behavior.
If both agents use aggressive slogans (e.g., "Don't buy their toxic bread!"), the sentiment shifts to "Tense". If they focus on quality, it becomes "Optimistic". This sentiment is then fed back into the AI's next prompt, creating a feedback loop that influences their future strategies.
Visualizing the War
We used Recharts to provide a real-time analytics dashboard. Seeing the balance and sales trends over time helps the user understand which AI strategy is winning—is it the low-price "price war" or the high-margin "luxury branding"?
Lessons Learned
- JSON Mode is a Life Saver: When building AI-driven apps, structured output is non-negotiable.
- Refs for High-Frequency Updates: React state is great for UI, but for 60fps simulations,
useRef+requestAnimationFrameis the way to go. - Parallelize AI Calls: Using
Promise.allfor multiple AI calls significantly reduces the "thinking" time between turns.
What's Next?
The "AI Price War" is just the beginning. We're looking at adding:
- Multi-Merchant Support: A village with 5+ competing shops.
- Human-in-the-Loop: Let a user play against the AI.
- Persistent Worlds: Save the market state to a database for long-running simulations.
Check out the full source code on GitHub and join the conversation!

Top comments (0)