using System.Collections.Immutable; using CardGame.Core; var cardsDir = ResolveContent("cards"); var packsDir = ResolveContent("packs"); var catalog = CardJson.LoadCatalog(cardsDir); var packs = PackJson.LoadDirectory(packsDir); var seed = ParseSeed(args); var auto = args.Any(a => a is "--auto" or "-a"); var runMode = args.Any(a => a is "--run" or "-r"); var fights = ParseFights(args); if (runMode) { RunFull(seed, auto, fights, catalog, packs); } else { RunSingleBattle(seed, auto, catalog); } static void RunSingleBattle(ulong seed, bool auto, IReadOnlyDictionary catalog) { var state = MatchSetup.CreateStartingBattle(seed, catalog, shuffle: true); Console.WriteLine($"Card Roguelike CLI combat seed={seed} auto={auto}"); Console.WriteLine("Commands: number = choose action, a = auto-step, q = quit"); _ = PlayBattle(state, auto, playerIsHuman: !auto); } static void RunFull( ulong seed, bool auto, int fightCount, IReadOnlyDictionary catalog, IReadOnlyList packs) { var run = RunProgress.Start(seed, catalog); Console.WriteLine($"Card Roguelike CLI full run seed={seed} fights={fightCount} auto={auto}"); Console.WriteLine("Between fights: reward draft → Holder → simple upgrade → next fight."); Console.WriteLine($"Starting owned={run.Owned.Count}"); for (var i = 0; i < fightCount; i++) { run.FightIndex = i; Console.WriteLine(); Console.WriteLine($"===== Fight {i + 1}/{fightCount} owned={run.Owned.Count} heal={run.Stats.HealReceived} atk={run.Stats.AttacksDeclared} ====="); var state = MatchSetup.CreateFromRun(run, catalog, shuffle: true); state = PlayBattle(state, auto, playerIsHuman: !auto); if (!state.IsOver) { Console.WriteLine("Battle stopped early; run aborted."); return; } run.SyncStatsFrom(state, 0); if (state.WinnerIndex != 0) { Console.WriteLine($"Defeat on fight {i + 1}. Wins={run.Wins}"); return; } run.Wins++; Console.WriteLine($"Victory. Wins={run.Wins}"); if (i + 1 >= fightCount) { break; } OfferReward(run, state, catalog, packs, auto); OfferUpgrade(run, catalog, auto); } Console.WriteLine(); Console.WriteLine($"Run complete. Wins={run.Wins}/{fightCount} owned={run.Owned.Count}"); } static GameState PlayBattle(GameState state, bool forceAuto, bool playerIsHuman) { PrintState(state); var steps = 0; const int maxActions = 200; while (!state.IsOver && steps < maxActions) { var legal = RulesEngine.GetLegalActions(state); if (legal.IsDefaultOrEmpty) { break; } GameAction action; var autoNow = forceAuto || state.ActivePlayerIndex == 1 || !playerIsHuman; if (autoNow) { action = TurnBeamSearch.ChooseWithTrace(state, legal).Action; Console.WriteLine($">> P{state.ActivePlayerIndex} {Describe(state, action)}"); } else { PrintActions(state, legal); Console.Write("P0> "); var line = Console.ReadLine(); if (line is null || line.Equals("q", StringComparison.OrdinalIgnoreCase)) { break; } if (line.Equals("a", StringComparison.OrdinalIgnoreCase)) { action = TurnBeamSearch.ChooseWithTrace(state, legal).Action; Console.WriteLine($">> P0 {Describe(state, action)}"); } else if (int.TryParse(line, out var index) && index >= 1 && index <= legal.Length) { action = legal[index - 1]; } else { Console.WriteLine("Invalid input."); continue; } } state = RulesEngine.Execute(state, action); steps++; PrintState(state); } if (state.IsOver) { Console.WriteLine($"Winner=P{state.WinnerIndex} turns={state.TurnNumber} P0={state.Players[0].Life} P1={state.Players[1].Life}"); } else { Console.WriteLine($"Stopped turns={state.TurnNumber} P0={state.Players[0].Life} P1={state.Players[1].Life}"); } return state; } static void OfferReward( RunProgress run, GameState battle, IReadOnlyDictionary catalog, IReadOnlyList packs, bool auto) { var pool = RunRewards.ActivePool(battle, 0, packs); var offer = RunRewards.RollOffer(run.Rng, pool, RunProgress.RewardChoices); Console.WriteLine(); Console.WriteLine($"Reward pool size={pool.Count} unlocked packs={Packs.Unlocked(battle, 0, packs).Count}"); if (offer.Count == 0) { Console.WriteLine("No reward cards available."); return; } for (var i = 0; i < offer.Count; i++) { var id = offer[i]; var name = catalog.TryGetValue(id, out var def) ? def.Name : id; Console.WriteLine($" {i + 1}. {name} ({id})"); } Console.WriteLine(" 0. Skip reward"); var pick = auto ? 1 : ReadIndex("Reward>", 0, offer.Count); if (pick <= 0) { Console.WriteLine("Skipped reward."); return; } var cardId = offer[pick - 1]; if (!catalog.TryGetValue(cardId, out var chosen)) { Console.WriteLine($"Missing card '{cardId}'."); return; } if (run.Owned.Count >= RunProgress.MaxOwned) { Console.WriteLine($"Owned at max ({RunProgress.MaxOwned}). Choose a card to remove:"); PrintOwned(run, catalog); var remove = auto ? 0 : ReadIndex("Remove>", 0, run.Owned.Count - 1); if (auto) { remove = run.Owned.Count - 1; } RunRewards.RemoveAt(run, remove); Console.WriteLine($"Removed index {remove}."); } var holders = RunRewards.HolderChoices(cardId); string holder; if (holders.Count == 1) { holder = holders[0]; } else if (auto) { holder = holders[0]; } else { Console.WriteLine("Assign Holder:"); for (var i = 0; i < holders.Count; i++) { Console.WriteLine($" {i + 1}. {holders[i]}"); } var h = ReadIndex("Holder>", 1, holders.Count); holder = holders[h - 1]; } RunRewards.AddCard(run, chosen, holder); Console.WriteLine($"Added {chosen.Name} holder={holder.Split('.').Last()} owned={run.Owned.Count}"); } static void OfferUpgrade(RunProgress run, IReadOnlyDictionary catalog, bool auto) { Console.WriteLine(); Console.WriteLine("Upgrade one owned card (or skip):"); PrintOwned(run, catalog); Console.WriteLine(" -1. Skip upgrade"); int cardIndex; if (auto) { cardIndex = -1; for (var i = 0; i < run.Owned.Count; i++) { var o = run.Owned[i]; if (!catalog.TryGetValue(o.DefinitionId, out var d)) { continue; } if (RunRewards.LegalUpgrades(o, d).Any(u => u != SimpleUpgrade.Skip)) { cardIndex = i; break; } } } else { cardIndex = ReadIndex("Card# (-1 skip)>", -1, run.Owned.Count - 1); } if (cardIndex < 0) { Console.WriteLine(auto ? "Auto: skipped upgrade." : "Skipped upgrade."); return; } var card = run.Owned[cardIndex]; if (!catalog.TryGetValue(card.DefinitionId, out var def)) { Console.WriteLine("Missing definition."); return; } var legal = RunRewards.LegalUpgrades(card, def); for (var i = 0; i < legal.Count; i++) { Console.WriteLine($" {i + 1}. {legal[i]}"); } int upIndex; if (auto) { upIndex = 0; for (var i = 0; i < legal.Count; i++) { if (legal[i] != SimpleUpgrade.Skip) { upIndex = i; break; } } } else { upIndex = ReadIndex("Upgrade>", 1, legal.Count) - 1; } var upgrade = legal[upIndex]; RunRewards.ApplyUpgrade(card, def, upgrade); Console.WriteLine($"Applied {upgrade} → {DescribeOwned(card, def)}"); } static void PrintOwned(RunProgress run, IReadOnlyDictionary catalog) { for (var i = 0; i < run.Owned.Count; i++) { var o = run.Owned[i]; catalog.TryGetValue(o.DefinitionId, out var def); Console.WriteLine($" {i}. {DescribeOwned(o, def)}"); } } static string DescribeOwned(OwnedCard o, CardDefinition? def) { var name = def?.Name ?? o.DefinitionId; var body = def?.Type == CardType.Creature || def?.Type == CardType.FieldCard ? $" {o.Attack}/{o.Health}" : ""; return $"{name}({o.Cost}){body}[{o.HolderId.Split('.').Last()}]"; } static int ReadIndex(string prompt, int min, int max) { while (true) { Console.Write(prompt + " "); var line = Console.ReadLine(); if (line is not null && int.TryParse(line, out var n) && n >= min && n <= max) { return n; } Console.WriteLine($"Enter {min}..{max}"); } } static string ResolveContent(string folder) { var path = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "content", folder)); if (!Directory.Exists(path)) { path = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "content", folder)); } return path; } static ulong ParseSeed(string[] args) { for (var i = 0; i < args.Length - 1; i++) { if (args[i] is "--seed" or "-s" && ulong.TryParse(args[i + 1], out var seed)) { return seed; } } return 20261006UL; } static int ParseFights(string[] args) { for (var i = 0; i < args.Length - 1; i++) { if (args[i] is "--fights" or "-f" && int.TryParse(args[i + 1], out var n) && n > 0) { return n; } } return RunProgress.DefaultFightCount; } static void PrintState(GameState state) { Console.WriteLine(); Console.WriteLine( $"--- turn {state.TurnNumber} active=P{state.ActivePlayerIndex} P0={state.Players[0].Life}hp/{state.Players[0].Energy}e P1={state.Players[1].Life}hp/{state.Players[1].Energy}e ---"); PrintZone("P0 hand", state.Players[0].Hand, state); PrintZone("P1 hand", state.Players[1].Hand, state); Console.WriteLine("Board:"); foreach (var e in state.Board) { Console.WriteLine(" " + DescribeEntity(state, e)); } Console.WriteLine($"P0 deck={state.Players[0].Deck.Count} gy={state.Players[0].Graveyard.Count} | P1 deck={state.Players[1].Deck.Count} gy={state.Players[1].Graveyard.Count}"); } static void PrintZone(string title, List cards, GameState state) { Console.WriteLine(title + ": " + string.Join(", ", cards.Select(c => ShortCard(state, c)))); } static void PrintActions(GameState state, ImmutableArray legal) { Console.WriteLine("Legal:"); for (var i = 0; i < legal.Length; i++) { Console.WriteLine($" {i + 1}. {Describe(state, legal[i])}"); } } static string Describe(GameState state, GameAction action) => action switch { PlayCardAction play => $"Play {ShortCard(state, state.Find(play.CardId)!)}" + (play.Target is null ? "" : $" -> {DescribeTarget(state, play.Target)}") + (play.Target2 is null ? "" : $" & {DescribeTarget(state, play.Target2)}"), AttackAction attack => $"Attack {ShortCard(state, state.Find(attack.AttackerId)!)} -> {DescribeTarget(state, attack.Target)}", ActivateAction act => $"Activate {ShortCard(state, state.Find(act.SourceId)!)}" + (act.Target is null ? "" : $" -> {DescribeTarget(state, act.Target)}"), EndTurnAction => "EndTurn", _ => action.ToString() ?? "action" }; static string DescribeTarget(GameState state, TargetRef target) => target switch { PlayerTarget p => $"P{p.PlayerIndex}", EntityTarget e => ShortCard(state, state.Find(e.Id)!), _ => "?" }; static string ShortCard(GameState state, Entity e) { var atk = e.Definition.Type == CardType.Creature ? state.EffectiveAttack(e) : e.Attack; return e.Definition.Type switch { CardType.Creature => $"{e.Id}:{e.Definition.Name}({e.Cost}) {atk}/{e.Health}[{e.HolderId.Split('.').Last()}]", CardType.FieldCard => $"{e.Id}:{e.Definition.Name} CD{e.Countdown}{(e.IsSystem ? "*" : "")}", _ => $"{e.Id}:{e.Definition.Name}({e.Cost})" }; } static string DescribeEntity(GameState state, Entity e) => $"P{e.ControllerIndex} {ShortCard(state, e)} atkLeft={e.AttacksRemaining} actLeft={e.ActivatesRemaining}";