openthebox/src/OpenTheBox/Program.cs
Samuel Bouchet 04894a4906 Initial project setup: Open The Box CLI game
- .NET 10 console app with Spectre.Console and Loreline integration
- Black Box Sim architecture (simulation separated from presentation)
- Progressive CLI rendering (9 phases from basic to full layout)
- 25+ box definitions with weighted loot tables
- 100+ item definitions (meta, cosmetics, materials, adventure tokens)
- 9 Loreline adventures (Space, Medieval, Pirate, etc.)
- Bilingual content (EN/FR)
- Save/load system
- Game Design Document
2026-03-10 18:24:01 +01:00

422 lines
14 KiB
C#

using OpenTheBox.Core;
using OpenTheBox.Core.Enums;
using OpenTheBox.Core.Items;
using OpenTheBox.Data;
using OpenTheBox.Localization;
using OpenTheBox.Persistence;
using OpenTheBox.Rendering;
using OpenTheBox.Simulation;
using OpenTheBox.Simulation.Actions;
using OpenTheBox.Simulation.Events;
using OpenTheBox.Adventures;
namespace OpenTheBox;
public static class Program
{
private static GameState _state = null!;
private static ContentRegistry _registry = null!;
private static LocalizationManager _loc = null!;
private static SaveManager _saveManager = null!;
private static GameSimulation _simulation = null!;
private static RenderContext _renderContext = null!;
private static IRenderer _renderer = null!;
private static bool _running = true;
public static async Task Main(string[] args)
{
_saveManager = new SaveManager();
_loc = new LocalizationManager(Locale.EN);
_renderContext = new RenderContext();
_renderer = RendererFactory.Create(_renderContext);
await MainMenuLoop();
}
private static async Task MainMenuLoop()
{
while (_running)
{
_renderer.Clear();
_renderer.ShowMessage("========================================");
_renderer.ShowMessage(" OPEN THE BOX");
_renderer.ShowMessage("========================================");
_renderer.ShowMessage("");
_renderer.ShowMessage(_loc.Get("game.subtitle"));
_renderer.ShowMessage("");
var options = new List<string>
{
_loc.Get("menu.new_game"),
_loc.Get("menu.load_game"),
_loc.Get("menu.language"),
_loc.Get("menu.quit")
};
int choice = _renderer.ShowSelection("", options);
switch (choice)
{
case 0: await NewGame(); break;
case 1: await LoadGame(); break;
case 2: ChangeLanguage(); break;
case 3: _running = false; break;
}
}
}
private static async Task NewGame()
{
string name = _renderer.ShowTextInput(_loc.Get("prompt.name"));
if (string.IsNullOrWhiteSpace(name)) name = "BoxOpener";
_state = GameState.Create(name, _loc.CurrentLocale);
InitializeGame();
var starterBox = ItemInstance.Create("box_starter");
_state.AddItem(starterBox);
_renderer.ShowMessage("");
_renderer.ShowMessage($"Welcome, {name}!");
_renderer.ShowMessage(_loc.Get("box.starter.desc"));
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
await GameLoop();
}
private static async Task LoadGame()
{
var slots = _saveManager.ListSlots();
if (slots.Count == 0)
{
_renderer.ShowMessage(_loc.Get("save.no_saves"));
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
return;
}
var options = slots.Select(s => $"{s.Name} ({s.SavedAt:yyyy-MM-dd HH:mm})").ToList();
options.Add(_loc.Get("menu.back"));
int choice = _renderer.ShowSelection(_loc.Get("save.choose_slot"), options);
if (choice >= slots.Count) return;
var loaded = _saveManager.Load(slots[choice].Name);
if (loaded == null)
{
_renderer.ShowError("Failed to load save.");
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
return;
}
_state = loaded;
_loc.Change(_state.CurrentLocale);
InitializeGame();
_renderer.ShowMessage(_loc.Get("misc.welcome_back", _state.PlayerName));
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
await GameLoop();
}
private static void InitializeGame()
{
_registry = ContentRegistry.LoadFromFiles(
"content/data/items.json",
"content/data/boxes.json",
"content/data/interactions.json"
);
_simulation = new GameSimulation(_registry);
_renderContext = RenderContext.FromGameState(_state);
_renderer = RendererFactory.Create(_renderContext);
}
private static void ChangeLanguage()
{
var options = new List<string> { "English", "Francais" };
int choice = _renderer.ShowSelection(_loc.Get("menu.language"), options);
var newLocale = choice == 0 ? Locale.EN : Locale.FR;
_loc.Change(newLocale);
if (_state != null)
_state.CurrentLocale = newLocale;
_renderer = RendererFactory.Create(_renderContext);
}
private static async Task GameLoop()
{
while (_running)
{
_renderer.Clear();
_renderer.ShowGameState(_state, _renderContext);
var actions = BuildActionList();
if (actions.Count == 0)
{
_renderer.ShowMessage(_loc.Get("error.no_boxes"));
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
break;
}
int choice = _renderer.ShowSelection(
_loc.Get("prompt.choose_action"),
actions.Select(a => a.label).ToList());
await ExecuteAction(actions[choice].action);
}
}
private static List<(string label, string action)> BuildActionList()
{
var actions = new List<(string label, string action)>();
var boxes = _state.Inventory.Where(i => _registry.IsBox(i.DefinitionId)).ToList();
if (boxes.Count > 0)
actions.Add((_loc.Get("action.open_box") + $" ({boxes.Count})", "open_box"));
if (_state.Inventory.Count > 0)
actions.Add((_loc.Get("action.inventory"), "inventory"));
if (_state.UnlockedAdventures.Count > 0)
actions.Add((_loc.Get("action.adventure"), "adventure"));
if (_state.UnlockedCosmetics.Count > 0)
actions.Add((_loc.Get("action.appearance"), "appearance"));
actions.Add((_loc.Get("action.save"), "save"));
actions.Add((_loc.Get("action.quit"), "quit"));
return actions;
}
private static async Task ExecuteAction(string action)
{
switch (action)
{
case "open_box": await OpenBoxAction(); break;
case "inventory": ShowInventory(); break;
case "adventure": await StartAdventure(); break;
case "appearance": ChangeAppearance(); break;
case "save": SaveGame(); break;
case "quit": _running = false; break;
}
}
private static async Task OpenBoxAction()
{
var boxes = _state.Inventory.Where(i => _registry.IsBox(i.DefinitionId)).ToList();
if (boxes.Count == 0)
{
_renderer.ShowMessage(_loc.Get("box.no_boxes"));
return;
}
var boxNames = boxes.Select(b =>
_loc.Get(_registry.GetItem(b.DefinitionId)?.NameKey ?? b.DefinitionId)).ToList();
boxNames.Add(_loc.Get("menu.back"));
int choice = _renderer.ShowSelection(_loc.Get("prompt.choose_box"), boxNames);
if (choice >= boxes.Count) return;
var boxInstance = boxes[choice];
var openAction = new OpenBoxAction(boxInstance.Id)
{
BoxDefinitionId = boxInstance.DefinitionId
};
var events = _simulation.ProcessAction(openAction, _state);
await RenderEvents(events);
}
private static async Task RenderEvents(List<GameEvent> events)
{
foreach (var evt in events)
{
switch (evt)
{
case BoxOpenedEvent boxEvt:
var boxDef = _registry.GetBox(boxEvt.BoxId);
_renderer.ShowBoxOpening(
_loc.Get(boxDef?.NameKey ?? boxEvt.BoxId),
boxDef?.Rarity.ToString() ?? "Common");
break;
case ItemReceivedEvent itemEvt:
_state.AddItem(itemEvt.Item);
var itemDef = _registry.GetItem(itemEvt.Item.DefinitionId);
_renderer.ShowLootReveal(
[
(
_loc.Get(itemDef?.NameKey ?? itemEvt.Item.DefinitionId),
(itemDef?.Rarity ?? ItemRarity.Common).ToString(),
(itemDef?.Category ?? ItemCategory.Box).ToString()
)
]);
break;
case UIFeatureUnlockedEvent uiEvt:
_renderContext.Unlock(uiEvt.Feature);
_renderer = RendererFactory.Create(_renderContext);
var featureKey = $"meta.{uiEvt.Feature.ToString().ToLower()}";
_renderer.ShowUIFeatureUnlocked(
_loc.Get("meta.unlocked", _loc.Get(featureKey)));
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
break;
case InteractionTriggeredEvent interEvt:
_renderer.ShowInteraction(_loc.Get(interEvt.DescriptionKey));
break;
case ResourceChangedEvent resEvt:
var resName = _loc.Get($"resource.{resEvt.Type.ToString().ToLower()}");
_renderer.ShowMessage($"{resName}: {resEvt.OldValue} -> {resEvt.NewValue}");
break;
case MessageEvent msgEvt:
_renderer.ShowMessage(_loc.Get(msgEvt.MessageKey, msgEvt.Args ?? []));
break;
case ChoiceRequiredEvent choiceEvt:
_renderer.ShowSelection(_loc.Get(choiceEvt.Prompt), choiceEvt.Options);
break;
case LootTableModifiedEvent:
_renderer.ShowMessage(_loc.Get("interaction.key_no_match"));
break;
case AdventureStartedEvent advEvt:
await RunAdventure(advEvt.Theme);
break;
}
}
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
}
private static void ShowInventory()
{
_renderer.Clear();
if (_state.Inventory.Count == 0)
{
_renderer.ShowMessage("Your inventory is empty. Open more boxes!");
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
return;
}
var grouped = _state.Inventory
.GroupBy(i => i.DefinitionId)
.Select(g =>
{
var def = _registry.GetItem(g.Key);
return (
name: _loc.Get(def?.NameKey ?? g.Key),
rarity: (def?.Rarity ?? ItemRarity.Common).ToString(),
category: (def?.Category ?? ItemCategory.Box).ToString()
);
})
.OrderBy(i => i.category)
.ThenBy(i => i.name)
.ToList();
_renderer.ShowLootReveal(grouped);
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
}
private static async Task StartAdventure()
{
var available = _state.UnlockedAdventures.ToList();
if (available.Count == 0)
{
_renderer.ShowMessage("No adventures available yet. Keep opening boxes!");
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
return;
}
var options = available.Select(a =>
{
bool completed = _state.CompletedAdventures.Contains(a.ToString());
return (completed ? "[Done] " : "") + a.ToString();
}).ToList();
options.Add(_loc.Get("menu.back"));
int choice = _renderer.ShowSelection(_loc.Get("action.adventure"), options);
if (choice >= available.Count) return;
await RunAdventure(available[choice]);
}
private static async Task RunAdventure(AdventureTheme theme)
{
try
{
var adventureEngine = new AdventureEngine(_renderer, _loc);
var events = await adventureEngine.PlayAdventure(theme, _state);
foreach (var evt in events)
{
if (evt.Kind == GameEventKind.ItemGranted)
_state.AddItem(ItemInstance.Create(evt.TargetId, evt.Amount));
}
_renderer.ShowMessage(_loc.Get("adventure.completed"));
}
catch (FileNotFoundException)
{
_renderer.ShowMessage($"Adventure '{theme}' is coming soon! The boxes are still being assembled.");
}
catch (Exception ex)
{
_renderer.ShowError($"Adventure error: {ex.Message}");
}
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
}
private static void ChangeAppearance()
{
var cosmeticItems = _state.Inventory
.Where(i =>
{
var def = _registry.GetItem(i.DefinitionId);
return def?.Category == ItemCategory.Cosmetic && def.CosmeticSlot.HasValue;
})
.ToList();
if (cosmeticItems.Count == 0)
{
_renderer.ShowMessage("No cosmetics available yet. Open Style Boxes!");
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
return;
}
var options = cosmeticItems.Select(i =>
{
var def = _registry.GetItem(i.DefinitionId);
return $"[{def?.CosmeticSlot}] {_loc.Get(def?.NameKey ?? i.DefinitionId)}";
}).ToList();
options.Add(_loc.Get("menu.back"));
int choice = _renderer.ShowSelection(_loc.Get("action.appearance"), options);
if (choice >= cosmeticItems.Count) return;
var action = new EquipCosmeticAction(cosmeticItems[choice].Id);
var events = _simulation.ProcessAction(action, _state);
foreach (var evt in events)
{
if (evt is CosmeticEquippedEvent cosEvt)
_renderer.ShowMessage($"Equipped {cosEvt.Slot}: {cosEvt.NewValue}");
else if (evt is MessageEvent msgEvt)
_renderer.ShowMessage(_loc.Get(msgEvt.MessageKey, msgEvt.Args ?? []));
}
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
}
private static void SaveGame()
{
_renderer.ShowMessage(_loc.Get("save.saving"));
_saveManager.Save(_state, _state.PlayerName);
_renderer.ShowMessage(_loc.Get("save.saved", _state.PlayerName));
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
}
}