Update ShowGameState test to expect box counter output (added in proposals_2.md). Add refactoring_plan.md documenting Black Box Sim pattern violations and prioritized fixes.
8.6 KiB
Refactoring Plan — Black Box Sim Compliance
Objectif
Rendre le code conforme au pattern Black Box Sim décrit dans le README :
Input → GameAction → GameSimulation (ZERO I/O, pure logic) → GameEvent[] → IRenderer
La simulation ne lit jamais d'entrée et n'écrit jamais de sortie. Elle reçoit des actions, elle retourne des événements. Program.cs orchestre l'entrée/sortie et connecte le tout.
Violations actuelles et corrections proposées
1. CraftingEngine fonctionne hors de GameSimulation (CRITIQUE)
Problème : CraftingEngine est instancié directement dans Program.cs (ligne 27) et appelé en dehors de GameSimulation via TickCraftingJobs() et CollectCrafting(). De plus, un MetaEngine est créé ad-hoc dans CollectCrafting() pour traiter les items craftés.
Solution :
- Internaliser
CraftingEnginedansGameSimulation - Ajouter
TickCraftingActionetCollectCraftingActioncomme GameActions GameSimulation.ProcessAction(TickCraftingAction)appelleCraftingEngine.TickJobs()en interneGameSimulation.ProcessAction(CollectCraftingAction)appelleCollectCompleted()+MetaEngine+AutoCraftCheck()en interne- Supprimer
_craftingEnginedeProgram.cs
Fichiers : Program.cs, GameSimulation.cs, CraftingEngine.cs, GameAction.cs
2. AdventureEngine appelle directement le renderer (CRITIQUE)
Problème : AdventureEngine reçoit un IRenderer dans son constructeur et appelle ShowAdventureDialogue(), ShowAdventureChoice(), WaitForKeyPress() directement. Ceci viole fondamentalement le pattern car la simulation fait de l'I/O.
Solution :
- Introduire un
IAdventureUIinterface avec les méthodes de callback :ShowDialogue(string? character, string text)int ShowChoice(List<string> options, List<string>? hints)ShowMessage(string message)
Program.csfournit l'implémentation deIAdventureUIqui délègue auIRendererAdventureEngineretourne desAdventureEventpour les changements d'état (items, resources) au lieu d'appelerShowMessage()pour les effets de jeu- Alternative : Garder le callback pour le dialogue interactif (nécessaire car Loreline est bloquant) mais retirer tous les appels
ShowMessagedes fonctions custom Loreline (grantItem, addResource, removeItem, markSecretBranch) — ces effets sont déjà retournés commeGameEventResult
Fichiers : AdventureEngine.cs, Program.cs, nouveau IAdventureUI.cs
3. Program.cs mute directement GameState (IMPORTANT)
Problème : Plusieurs endroits dans Program.cs modifient GameState sans passer par GameSimulation :
_state.AddItem(starterBox)dansNewGame()(ligne 261)_state.AddItem(...)dansRunAdventure()(ligne 875)_state.CurrentLocale = newLocaledansChangeLanguage()(ligne 358)_state.EventLog.Add(message)dansAddEventLog()(ligne 1054)
Solution :
- Créer
NewGameAction→ retourne unItemReceivedEventavec la starter box - Les événements d'aventure (ItemGranted) devraient être traités par
GameSimulation.ProcessAction(AdventureResultAction)au lieu de muter directement l'inventaire - Le changement de locale passe déjà par
ChangeLocaleActionquand fait en jeu — s'assurer queChangeLanguage()utilise aussi ce chemin - L'EventLog devrait être alimenté par le renderer ou un composant de présentation séparé, pas en mutant
GameState
Fichiers : Program.cs, GameSimulation.cs, GameAction.cs
4. Les renderers dépendent de ContentRegistry (IMPORTANT)
Problème : SpectreRenderer, TerminalGuiRenderer, InventoryPanel, CraftingPanel reçoivent tous un ContentRegistry? et font des lookups d'ItemDefinition et BoxDefinition directement.
Solution :
- Créer un modèle de vue (ViewModel) dans le
RenderContextou à côté deGameState:record DisplayItem(string Name, string Rarity, string Category, int Qty, string? Description, ...); record DisplayCraftingJob(string Name, string Station, int ProgressPercent, bool IsComplete); - La résolution
DefinitionId → nom localisé + rarity + descriptionse fait UNE FOIS au moment du rendu, dans Program.cs ou un nouveauViewModelBuilder - Les renderers reçoivent des
DisplayItem[]au lieu deGameState + ContentRegistry - Phase transitoire : Garder le
ContentRegistrydans les panels pour l'instant mais créer leViewModelBuilderprogressivement
Fichiers : Nouveau Rendering/ViewModels/, InventoryPanel.cs, CraftingPanel.cs, SpectreRenderer.cs, TerminalGuiRenderer.cs
5. RenderEvents mélange logique et présentation (MODÉRÉ)
Problème : La méthode RenderEvents() dans Program.cs (lignes 475-605) fait :
- Du rendu (
_renderer.ShowXxx()) - De la logique (
_renderContext.Unlock(),RefreshRenderer()) - Du tracking (
AddEventLog()) - De l'orchestration (
await RunAdventure())
Solution :
- Séparer en deux passes :
- Pass logique : Traite les événements qui modifient l'état du programme (unlock features, update render context)
- Pass présentation : Appelle le renderer pour chaque événement à afficher
- Déplacer
RefreshRenderer()dans la pass logique, avant la pass présentation - L'EventLog devrait être alimenté dans la pass logique, pas en parallèle du rendu
Fichiers : Program.cs
6. Program.cs connaît les types de domaine (MODÉRÉ)
Problème : Le game loop inspecte directement ItemCategory, ItemDefinition.ResourceType, CosmeticSlot, etc. pour construire les menus et déterminer les actions disponibles.
Solution :
- Ajouter une méthode
GameSimulation.GetAvailableActions(GameState) → List<AvailableAction>qui retourne les actions possibles avec leurs labels - Ou : ajouter des propriétés helper sur
GameState:HasBoxes,HasConsumables,HasCosmeticsToEquip,HasCompletedCrafting
- L'objectif n'est pas d'éliminer toute connaissance des catégories dans Program.cs (c'est la couche d'orchestration) mais de réduire les requêtes directes au registre
Fichiers : GameSimulation.cs ou GameState.cs, Program.cs
7. Portrait dupliqué entre les renderers (FAIBLE)
Problème : TerminalGuiRenderer (lignes 614-677) duplique tout l'art ASCII du portrait qui existe déjà dans PortraitPanel.cs.
Solution :
- Extraire les méthodes
GetHairArt(),GetEyeArt(), etc. dans un helper statique partagéPortraitArt PortraitPaneletTerminalGuiRendererutilisent tous les deuxPortraitArt.GetHairArt(), etc.- Alternative :
PortraitPanelexpose une méthodeRenderPlainText()queTerminalGuiRendererpeut appeler
Fichiers : Nouveau Rendering/Panels/PortraitArt.cs, PortraitPanel.cs, TerminalGuiRenderer.cs
8. EventLog est de l'état de présentation dans GameState (FAIBLE)
Problème : GameState.EventLog est une liste de strings de présentation ("★ Text Colors", "+ Bronze [Uncommon]") stockée dans le modèle de simulation. Elle n'est pas persistée (pas dans les saves), confirmant qu'il s'agit d'état de présentation.
Solution :
- Déplacer
EventLoghors deGameStatedans un composant de présentation séparé (ex:ChatLogdans le namespaceRendering) Program.csalimente leChatLogdans sa pass de rendu des événements- Les renderers reçoivent le
ChatLogau lieu de lirestate.EventLog
Fichiers : GameState.cs, nouveau Rendering/ChatLog.cs, Program.cs, ChatPanel.cs, TerminalGuiRenderer.cs
Ordre de priorité recommandé
| Priorité | Tâche | Impact | Effort |
|---|---|---|---|
| 1 | CraftingEngine dans GameSimulation | Critique | Moyen |
| 2 | Mutations directes de GameState | Important | Faible |
| 3 | RenderEvents deux passes | Modéré | Moyen |
| 4 | ViewModels pour renderers | Important | Élevé |
| 5 | AdventureEngine I/O callbacks | Critique | Élevé |
| 6 | GetAvailableActions helper | Modéré | Faible |
| 7 | Portrait partagé | Faible | Faible |
| 8 | EventLog hors de GameState | Faible | Faible |
Principes directeurs
- GameSimulation est une boîte noire — elle reçoit des
GameAction, elle retourne desGameEvent[]. Rien d'autre. - Les renderers ne connaissent pas le domaine — ils reçoivent des données pré-formatées (ViewModels) et les affichent.
- Program.cs est le câblage — il connecte l'input au simulateur et le simulateur au renderer. Il ne contient pas de logique de jeu.
- Les mutations de GameState passent par GameSimulation — aucune écriture directe depuis Program.cs.
- Pas de refactoring big-bang — chaque tâche peut être faite indépendamment et testée.