feat: integrate AI-powered deck building and card picking using Google Gemini.

This commit is contained in:
2025-12-20 16:18:11 +01:00
parent 664d0e838d
commit 2794ce71aa
13 changed files with 735 additions and 15 deletions

View File

@@ -1,3 +1,4 @@
import 'dotenv/config';
import express, { Request, Response } from 'express';
import { createServer } from 'http';
import { Server } from 'socket.io';
@@ -12,6 +13,7 @@ import { PackGeneratorService } from './services/PackGeneratorService';
import { CardParserService } from './services/CardParserService';
import { PersistenceManager } from './managers/PersistenceManager';
import { RulesEngine } from './game/RulesEngine';
import { GeminiService } from './services/GeminiService';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -81,6 +83,19 @@ app.get('/api/health', (_req: Request, res: Response) => {
res.json({ status: 'ok', message: 'Server is running' });
});
// AI Routes
app.post('/api/ai/pick', async (req: Request, res: Response) => {
const { pack, pool, suggestion } = req.body;
const result = await GeminiService.getInstance().generatePick(pack, pool, suggestion);
res.json({ pick: result });
});
app.post('/api/ai/deck', async (req: Request, res: Response) => {
const { pool, suggestion } = req.body;
const result = await GeminiService.getInstance().generateDeck(pool, suggestion);
res.json({ deck: result });
});
// Serve Frontend in Production
if (process.env.NODE_ENV === 'production') {
const distPath = path.resolve(process.cwd(), 'dist');
@@ -231,6 +246,67 @@ const draftInterval = setInterval(() => {
updates.forEach(({ roomId, draft }) => {
io.to(roomId).emit('draft_update', draft);
// Check for Bot Readiness Sync (Deck Building Phase)
if (draft.status === 'deck_building') {
const room = roomManager.getRoom(roomId);
if (room) {
let roomUpdated = false;
Object.values(draft.players).forEach(dp => {
if (dp.isBot && dp.deck && dp.deck.length > 0) {
const roomPlayer = room.players.find(rp => rp.id === dp.id);
// Sync if not ready
if (roomPlayer && !roomPlayer.ready) {
const updated = roomManager.setPlayerReady(roomId, dp.id, dp.deck);
if (updated) roomUpdated = true;
}
}
});
if (roomUpdated) {
io.to(roomId).emit('room_update', room);
// Check if EVERYONE is ready to start game automatically
const activePlayers = room.players.filter(p => p.role === 'player');
if (activePlayers.length > 0 && activePlayers.every(p => p.ready)) {
console.log(`All players ready (including bots) in room ${roomId}. Starting game.`);
room.status = 'playing';
io.to(roomId).emit('room_update', room);
const game = gameManager.createGame(roomId, room.players);
// Populate Decks
activePlayers.forEach(p => {
if (p.deck) {
p.deck.forEach((card: any) => {
gameManager.addCardToGame(roomId, {
ownerId: p.id,
controllerId: p.id,
oracleId: card.oracle_id || card.id,
name: card.name,
imageUrl: card.image || card.image_uris?.normal || card.card_faces?.[0]?.image_uris?.normal || "",
zone: 'library',
typeLine: card.typeLine || card.type_line || '',
oracleText: card.oracleText || card.oracle_text || '',
manaCost: card.manaCost || card.mana_cost || '',
keywords: card.keywords || [],
power: card.power,
toughness: card.toughness,
damageMarked: 0,
controlledSinceTurn: 0
});
});
}
});
const engine = new RulesEngine(game);
engine.startGame();
io.to(roomId).emit('game_update', game);
}
}
}
}
// Check for forced game start (Deck Building Timeout)
if (draft.status === 'complete') {
const room = roomManager.getRoom(roomId);
@@ -478,6 +554,8 @@ io.on('connection', (socket) => {
if (!context) return;
const { room, player } = context;
console.log(`[Socket] 📩 Recv pick_card: Player ${player.name} (ID: ${player.id}) picked ${cardId}`);
const draft = draftManager.pickCard(room.id, player.id, cardId);
if (draft) {
io.to(room.id).emit('draft_update', draft);