Files

118 lines
8.5 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using HotUpdate.Gameplay;
using TMPro;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
public static class VerifyPrototype
{
sealed class MemoryStore : IPrototypeStore
{
public PrototypeSave Save;
public bool Fail;
public PrototypeSave Read() => Save == null ? null : JsonUtility.FromJson<PrototypeSave>(JsonUtility.ToJson(Save));
public void Write(PrototypeSave state)
{
if (Fail) throw new InvalidOperationException("Simulated storage failure.");
Save = JsonUtility.FromJson<PrototypeSave>(JsonUtility.ToJson(state));
}
}
static void Check(bool condition, string message) { if (!condition) throw new Exception(message); }
static PrototypeBattle Finish(PrototypeGame game, string id)
{
var battle = new PrototypeBattle(id, game);
for (int i = 0; i < 2500 && !battle.Finished; i++) battle.Tick();
Check(battle.Finished, "Battle did not terminate."); return battle;
}
public static string Domain()
{
var bins = new HashSet<int>();
foreach (int aim in new[] { -35, -20, -10, 0, 10, 20, 35 })
{
var first = PrototypePinball.Simulate(aim); var second = PrototypePinball.Simulate(aim);
Check(first.Path.SequenceEqual(second.Path) && first.Candy == second.Candy, "Pinball not deterministic.");
Check(first.Path.Count > 20 && first.Path.Count <= 601, "Invalid pinball duration.");
Check(first.Path.All(p => p.x >= .024f && p.x <= .976f), "Ball left the board.");
Check(first.Candy == PrototypePinball.Candy[first.Bin] && first.Coins == PrototypePinball.Coins[first.Bin], "Reward not tied to landing.");
bins.Add(first.Bin);
}
Check(bins.Count >= 2, "Aiming has no effect on landing.");
long clock = 1000;
var recoveryStore = new MemoryStore();
var recover = new PrototypeGame(recoveryStore, () => clock);
clock += 599; Check(!recover.RefreshBalls() && recover.State.balls == 20, "Recovery granted early.");
clock += 1; Check(recover.RefreshBalls() && recover.State.balls == 21, "Recovery did not grant one ball.");
clock += 99999; Check(recover.RefreshBalls() && recover.State.balls == 30, "Recovery exceeded cap.");
clock += 99999; Check(!recover.RefreshBalls(), "Full stock banked more balls.");
recover.BeginShot("leave-cap", 0, out _); recover.CompleteShot("leave-cap");
Check(!recover.RefreshBalls() && recover.State.balls == 29, "Leaving cap granted paused-time recovery.");
clock -= 999; Check(!recover.RefreshBalls(), "Clock rollback granted currency.");
for (int aim = -35; aim <= 35; aim++)
{
var shot = PrototypePinball.Simulate(aim);
Check(shot.Path[shot.Path.Count - 1].y <= .035f, "Ball did not reach reward row for aim " + aim);
}
var store = new MemoryStore(); var game = new PrototypeGame(store);
Check(game.State.balls == 20 && game.State.stages[2] == 0, "Wrong new-player state.");
Check(!game.BeginShot("bad", 99, out _) && game.State.balls == 20, "Invalid aim charged currency.");
string beforeFailedWrite = JsonUtility.ToJson(game.State);
store.Fail = true;
try { game.BeginShot("fail", 0, out _); throw new Exception("Storage failure was ignored."); }
catch (InvalidOperationException) { Check(JsonUtility.ToJson(game.State) == beforeFailedWrite && string.IsNullOrEmpty(game.State.pendingShot), "Failed write mutated state."); }
store.Fail = false;
Check(game.BeginShot("recover", 0, out _) && game.State.balls == 19, "Shot not charged exactly once.");
Check(!game.BeginShot("other", 0, out _) && game.State.balls == 19, "Two in-flight shots allowed.");
game = new PrototypeGame(store); Check(game.State.pendingShot == "recover", "In-flight shot lost on reload.");
Check(game.CompleteShot("recover"), "Recovery did not settle."); int coin = game.State.coins;
Check(!game.CompleteShot("recover") && game.State.coins == coin && game.State.shots == 1, "Shot reward duplicated.");
Check(game.State.tickets == 1, "First-shot capture ticket missing.");
Check(game.Capture(out _) == 2 && game.State.stages[2] == 1 && game.State.tickets == 0, "New pet not captured.");
Check(game.Capture(out _) == -1, "Capture allowed without ticket.");
Check(game.Deploy(1, 0, out _) && game.State.formation[0] == 1 && game.State.formation[1] == 0, "Formation swap lost an occupant.");
Check(!game.Deploy(1, 2, out _), "Locked route accepted deployment.");
game.AutoDeploy();
var win = Finish(game, "win"); Check(win.Won && win.Durability > 0, "Recommended tutorial formation failed.");
var replay = Finish(game, "replay");
Check(win.Durability == replay.Durability && win.Defeated == replay.Defeated && win.Time == replay.Time, "Battle results differ for identical formation.");
coin = game.State.coins; int balls = game.State.balls;
Check(game.SettleBattle(win) && game.State.coins == coin + 80 && game.State.balls == balls + 3, "First clear reward incorrect.");
Check(!game.SettleBattle(win), "Same battle settled twice.");
coin = game.State.coins; Check(game.SettleBattle(replay) && game.State.coins == coin, "Repeat clear paid first-clear reward.");
Check(game.ClaimGoal(out _) && !game.ClaimGoal(out _), "Goal reward not once-only.");
var rich = new PrototypeGame(new MemoryStore { Save = new PrototypeSave { candy = 1000, coins = 1000, tickets = 1, level = 5, shards = new[] { 40, 0, 0 }, stages = new[] { 1, 1, 1 } } });
Check(rich.Evolve(0, out _) && rich.State.coins == 500 && rich.State.shards[0] == 0 && rich.Growth(0) == 1.5f, "Evolution cost or growth incorrect.");
Check(!rich.Evolve(0, out _), "Already-evolved pet evolved twice.");
Check(rich.Train(out _) && rich.State.level == 6 && rich.State.candy == 850, "Training cost incorrect.");
Check(rich.Capture(out _) >= 0 && rich.State.shards.Sum() == 20, "Duplicate capture did not convert.");
var empty = new PrototypeGame(new MemoryStore { Save = new PrototypeSave { formation = new[] { -1, -1, -1, -1, -1, -1 } } });
var loss = Finish(empty, "loss"); Check(!loss.Won && loss.Durability == 0, "Unprotected defence did not fail.");
Check(empty.SettleBattle(loss) && empty.State.coins == 0 && !empty.State.cleared, "Failure paid clear rewards.");
string key = "verify-" + Guid.NewGuid().ToString("N");
try
{
var disk = new PrototypeLocalStore(key);
disk.Write(new PrototypeSave { revision = 1, coins = 11 });
disk.Write(new PrototypeSave { revision = 2, coins = 22 });
Check(disk.Read().coins == 22, "Latest local snapshot not restored.");
PlayerPrefs.SetString("AIGame.G0.v1." + key + ".0", "corrupt");
Check(disk.Read().coins == 11, "Corrupt latest slot did not fall back to valid alternate.");
}
finally { PlayerPrefs.DeleteKey("AIGame.G0.v1." + key + ".0"); PlayerPrefs.DeleteKey("AIGame.G0.v1." + key + ".1"); PlayerPrefs.Save(); }
var noMoney = new PrototypeGame(new MemoryStore()); Check(!noMoney.Train(out _) && noMoney.State.level == 1, "Training accepted insufficient funds.");
foreach (string screen in new[] { "Pinball", "Pets", "Formation", "Battle" })
{
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Bundles/UI/Prefab/UI_Prototype" + screen + ".prefab");
Check(prefab != null, "Missing UI prefab " + screen);
Check(prefab.GetComponentsInChildren<TextMeshProUGUI>(true).All(t => t.font != null), "Missing TMP font.");
foreach (var image in prefab.GetComponentsInChildren<Image>(true).Where(i => i.type == Image.Type.Sliced))
Check(image.sprite != null && image.sprite.border.x > 0 && image.sprite.border.y > 0, "Invalid nine-slice: " + image.name);
}
Check(AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Bundles/Scenes/Prototype/PrototypeArena.prefab") != null, "Missing authored arena.");
return "G0 domain + prefab checks passed: input-dependent deterministic board; pending recovery; once-only rewards; write failure; capture/duplicates; formation; win/loss; training/evolution; fonts and nine-slices. Tutorial won in " + win.Time + "s at durability " + win.Durability + "; aim reaches " + bins.Count + " reward bins.";
}
}