Un mini-jeu Pokémon créé en 20 minutes avec l'IA ? C'est possible !
L'autre soir, j'ai voulu relever un défi : créer un mini-jeu Pokémon en .NET, sans base de données ni logique métier complexe, en moins de 30 minutes. Le secret ? Semantic Kernel et ses Kernel Functions, qui permettent de déléguer toute la logique à l'IA.
La démo en vidéo
Comment ça marche ?
C'est simple : chaque action du jeu (choix du Pokémon, combat, soin...) est gérée par une Kernel Function, un plugin IA qui prend ses décisions de manière autonome. Plus besoin d'écrire des centaines de lignes de conditions ni de gérer une base de données : tout est géré dans vos Kernel Functions et grâce aux modèles de données que vous avez définis.
Les fameuses Kernel Functions sont dans un plugin que j'ai créé et appelé directement dans mon service :
[KernelFunction("get_team")]
[Description("Affiche les Pokémon de l'équipe du joueur avec leurs statistiques.")]
public async Task<TeamResponseModel> GetTeamAsync()
{
return new TeamResponseModel
{
Team = _team,
MainPokemon = _mainPokemon?.Name ?? "Aucun Pokémon principal n'est défini."
};
}
[KernelFunction("heal_team")]
[Description("Soigne tous les Pokémon de l'équipe en restaurant leur santé maximale.")]
public async Task<string> HealTeamAsync()
{
_team.ForEach(p => p.Health = p.MaxHealth);
return "Pokémon soignés.";
}
[KernelFunction("get_main_pokemon")]
[Description("Affiche le Pokémon principal actuel.")]
public async Task<string> GetMainPokemonAsync()
{
if (_mainPokemon == null)
{
return "Non défini";
}
return $"Pokémon principal : {_mainPokemon.Name}.";
}
Avec juste ces quelques fonctions bien nommées et des descriptions claires (start_game, get_team, heal_team, set_main_pokemon, fight_wild_pokemon, catch_random_pokemon...), l'IA comprend seule quelle fonction appeler en fonction de la question posée.
Une seule route pour discuter avec le jeu
Côté API, j'utilise une seule route. Peu importe l'action que je veux faire (démarrer une partie, soigner l'équipe, afficher les Pokémon...), j'appelle toujours la même route en échangeant naturellement, comme si je parlais à un ami :
[HttpPost("play")]
public async Task<ActionResult<string>> PlayGame([FromBody] AskQuestionWithSessionCommand command)
{
if (string.IsNullOrWhiteSpace(command.Question))
{
return BadRequest(new { Error = "La question ne peut pas être vide." });
}
var result = await _gameService.PlayGameAsync(command);
return Ok(result);
}
Le service : là où la magie opère
Le service branche le plugin, laisse l'IA choisir elle-même ses fonctions avec FunctionChoiceBehavior.Auto(), et conserve l'historique de conversation dans un cache : l'IA garde le contexte du jeu entre chaque appel et peut jouer une même partie en continu.
public async Task<string> PlayGameAsync(AskQuestionWithSessionCommand command)
{
// Lier notre méthode au plugin qui contient nos kernel functions
_kernel.Plugins.AddFromType<GamePlugin>("Game");
// Récupérer le service de chat IA avec lequel on travaille
var chatService = _kernel.GetRequiredService<IChatCompletionService>();
// Définir les paramètres d'exécution du prompt
OpenAIPromptExecutionSettings settings = new()
{
// L'IA choisit elle-même quelle Kernel Function utiliser
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
};
// Récupérer ou créer un historique de chat pour la session actuelle
var chatHistory = _cache.GetOrCreate(command.SessionId, entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
var newHistory = new ChatHistory();
// Message système pour définir le rôle de l'IA dans le jeu
newHistory.AddSystemMessage(
"Tu es un maître du jeu Pokémon et tu t'appelles El Profesor. " +
"Tu dois aider le joueur à :\n" +
"- Gérer son équipe\n" +
"- Se battre contre des Pokémon sauvages\n" +
"- Capturer de nouveaux Pokémon."
);
return newHistory;
});
// Ajouter la question de l'utilisateur dans l'historique
chatHistory.AddUserMessage(command.Question);
// Générer une réponse en fonction du contexte
var result = await chatService.GetChatMessageContentAsync(
chatHistory,
executionSettings: settings,
kernel: _kernel
);
// Ajouter la réponse de l'IA dans l'historique
chatHistory.AddAssistantMessage(result.ToString());
return result.ToString();
}
Notez le message système : c'est lui qui donne son rôle à l'IA (« El Profesor », maître du jeu Pokémon).
Les modèles de données
Le plugin déclare l'état du jeu et le pool de Pokémon, entièrement en mémoire :
public class GamePlugin
{
private static List<PokemonModel> _team = new();
private static PokemonModel? _mainPokemon = null;
private static readonly List<PokemonModel> _pokemonPool = new()
{
new PokemonModel { Name = "Pikachu", MaxHealth = 50, Attack = 25, Defense = 12, Type = "électrique" },
new PokemonModel { Name = "Bulbizarre", MaxHealth = 50, Attack = 20, Defense = 15, Type = "plante" },
new PokemonModel { Name = "Salamèche", MaxHealth = 60, Attack = 22, Defense = 10, Type = "feu" },
new PokemonModel { Name = "Carapuce", MaxHealth = 50, Attack = 18, Defense = 18, Type = "eau" },
new PokemonModel { Name = "Dracolosse", MaxHealth = 120, Attack = 40, Defense = 20, Type = "dragon" },
// ... le reste du pool
};
// kernel functions ...
}
public class PokemonModel
{
public string Name { get; set; }
public int Health { get; set; }
public int MaxHealth { get; set; }
public int Attack { get; set; }
public int Defense { get; set; }
public string Type { get; set; }
}
Et la Kernel Function qui démarre une partie :
[KernelFunction("start_game")]
[Description("Démarre une nouvelle partie en attribuant 3 Pokémon aléatoires et définit un Pokémon principal.")]
public async Task<string> StartGameAsync()
{
var random = new Random();
_team = _pokemonPool.OrderBy(x => random.Next()).Take(3).ToList();
_team.ForEach(p => p.Health = p.MaxHealth);
_mainPokemon = _team.First();
return $"Equipe: {string.Join(", ", _team.Select(p => p.Name))} et pokemon principal: {_mainPokemon.Name}";
}
Le plus intéressant ?
- Il n'y a aucun échange avec une base de données : tout l'état du jeu vit en mémoire dans des objets C#.
- C'est l'IA qui décide seule quelle fonction appeler et comment elle doit répondre, selon la situation.
- L'IA est totalement autonome : elle comprend le contexte et choisit la meilleure action à faire.
Et elle ne se limite pas à exécuter bêtement une fonction. Elle enrichit chaque réponse de manière intelligente et surprenante : elle sait quelle fonction appeler ET comment enrichir sa réponse, sans que je le lui aie appris explicitement. Elle propose parfois des actions qu'on ne lui a même pas demandées, comme suggérer ce que le joueur pourrait faire ensuite.
Mon rôle ? Trois choses seulement
- Des modèles de données (comme les Pokémon et leurs stats) ;
- des règles simples ;
- des noms de fonctions clairs et des descriptions explicites pour que l'IA comprenne bien ce qu'elle peut faire.
Résultat : l'IA peut gérer un jeu entier toute seule, en répondant toujours de manière cohérente au joueur.
Les points forts de l'approche
Développement ultra rapide : une Kernel Function = quelques lignes de code + un prompt en langage naturel. L'IA s'occupe du reste.
Architecture légère et flexible : pas de base de données, pas de backend complexe.
Plugins réutilisables : les Kernel Functions sont comme des Lego. Créez-en une fois, réutilisez-la partout.
Contrôle total sur l'IA : chaque Kernel Function peut être paramétrée pour affiner son comportement.
Et on peut aller bien plus loin
En 20 minutes, j'ai créé un jeu fonctionnel, mais ce n'est que le début. Avec Semantic Kernel, vous pouvez aussi créer des embeddings pour la recherche intelligente ou automatiser des tâches complexes. C'est exactement ce type de briques que j'ai ensuite mis en production sur un vrai projet : un bot RAG qui interroge toute la base documentaire d'une entreprise.
Semantic Kernel prend en charge C#, Python et Java.
À retenir : avec quelques noms de fonctions bien choisis, un peu de code et des modèles de données simples, on obtient une IA capable de comprendre toute seule comment agir. Les possibilités sont infinies.
Cet article est tiré d'une de mes publications LinkedIn : rejoignez la discussion ↗