XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

AshfallFrontier

【Unity】灰烬边境,一款生存策略游戏

公开
关注 0 Fork 0 Star 0
UTF-8
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using Ashfall.Core;

namespace Ashfall.Specs
{
    internal sealed class JsonCodec : IWorldCodec
    {
        private readonly JsonSerializerOptions options = new JsonSerializerOptions { IncludeFields = true, IgnoreReadOnlyProperties = true };
        public string Encode(WorldState world) => JsonSerializer.Serialize(world, options);
        public WorldState Decode(string json) => JsonSerializer.Deserialize<WorldState>(json, options);
    }

    internal static class Program
    {
        private static int passed, failed;
        private static readonly List<string> failures = new List<string>();
        private static void Main(string[] args)
        {
            Check("catalog stable IDs and complete target counts", Catalog);
            Check("projection and moving-space round trips", ProjectionRoundTrips);
            Check("new brood player starts as a worm", WormStart);
            Check("worm metamorphosis is gated and transactional", WormEvolution);
            Check("brood form survives species changes and save", WormPersistence);
            Check("all races may drive either vehicle family", SharedDriving);
            Check("station claim race permits only one occupant", StationRace);
            Check("driver and gunner have exclusive independent groups", WeaponOwnership);
            Check("disconnect brakes vehicle and releases firing", Disconnect);
            Check("stale input stops firing and driving", StaleInput);
            Check("moving interiors preserve passenger local position", MovingInterior);
            Check("moving and airborne exits rejected", ExitSafety);
            Check("blocked exit leaves the player in original space", BlockedExit);
            Check("portal destination is revalidated at commit", PortalCommit);
            Check("walk into stairs changes floor without interaction", Stairs);
            Check("different floors cannot be damaged by planar overlap", FloorCombat);
            Check("building and vehicle hulls block exterior fire", ExteriorOcclusion);
            Check("worm bite and burrowing work from initial form", WormCombat);
            Check("mixed modules rejected without consuming inventory", ModuleCompatibility);
            Check("module replacement is idempotent and recovers old part", ModuleTransaction);
            Check("inventory transfers conserve count and reject negative amounts", InventoryTransactions);
            Check("production survives full output storage without duplicate output", Production);
            Check("offline world and disconnected production pause", OfflinePause);
            Check("research requires proximity, power and correct species", Research);
            Check("stairs create a playable upper floor, max four floors", Building);
            Check("building cannot enclose a player away from all exits", EscapeRoutes);
            Check("save backup recovers corruption without losing worm state", Backups);
            Check("invalid snapshots never overwrite good saves", InvalidSave);
            Check("null nested save data is rejected as corruption", NullSaveData);
            Check("restored seat falls back if another player occupies it", RestoreSeat);
            Check("fifth connected player is refused", FourPlayers);
            Check("malformed input never contaminates world coordinates", BadInput);
            Check("four-player 15-minute simulation preserves invariants", Soak);
            Console.WriteLine($"\n{passed} passed; {failed} failed");
            foreach (string failure in failures) Console.WriteLine(failure);
            Environment.ExitCode = failed == 0 ? 0 : 1;
        }
        private static void Check(string name, Action test)
        {
            try { test(); passed++; Console.WriteLine("PASS " + name); }
            catch (Exception ex) { failed++; failures.Add(name + ": " + ex.Message); Console.WriteLine("FAIL " + name + " — " + ex.Message); }
        }
        private static void Assert(bool condition, string message = "assertion failed") { if (!condition) throw new Exception(message); }
        private static void ExteriorOcclusion()
        {
            var s = Sim();
            Assert(s.Blocked(WorldFactory.At("world", 0, new V2(-32, 16)), new V2(-8, 16)));
            var v = s.World.Vehicle("crawler");
            var start = WorldFactory.At("world", 0, v.position + new V2(-10, 0));
            Assert(s.Blocked(start, v.position + new V2(10, 0)));
            Assert(!s.Blocked(start, v.position + new V2(10, 0), v.id));
        }
        private static void NullSaveData()
        {
            var world = WorldFactory.Create(); world.vehicles[0].stations.Add(null);
            bool rejected = false;
            try { SaveStore.Validate(world); } catch (InvalidDataException) { rejected = true; }
            Assert(rejected, "bad nested data must participate in save recovery");
        }
        private static void Close(float a, float b, float tolerance = .001f) => Assert(Math.Abs(a - b) <= tolerance, $"expected {b}, got {a}");
        private static GameSimulation Sim() => new GameSimulation(WorldFactory.Create(), ContentCatalog.CreateDefault());
        private static PlayerState Add(GameSimulation s, string id = "a", Species species = Species.Human) => s.Connect(id, id, species);
        private static ActionResult Do(GameSimulation s, PlayerState p, CommandKind kind, string target = "", string value = "", string slot = "", int amount = 1)
            => s.Execute(p.id, new GameCommand { sequence = p.lastCommand + 1, kind = kind, targetId = target, value = value, slot = slot, amount = amount });
        private static void At(PlayerState p, string space, int floor, float x, float y) => p.location = WorldFactory.At(space, floor, new V2(x, y));
        private static void Step(GameSimulation s, int count) { for (int i = 0; i < count; i++) s.Tick(GameSimulation.TickSeconds); }
        private static void Seat(GameSimulation s, PlayerState p, string id)
        { var seat = s.FindStation(id); p.location = seat.location.Copy(); Assert(Do(s, p, CommandKind.Interact, id).success); }
        private static void Catalog()
        {
            var c = ContentCatalog.CreateDefault(); c.Validate();
            foreach (var pair in new[] { ("hero", 8), ("weapon", 12), ("gene", 24), ("facility", 40), ("turret", 24), ("vehicle", 16), ("region", 6), ("boss", 6) })
                Assert(c.entries.Count(e => e.category == pair.Item1) == pair.Item2, pair.Item1);
            Assert(c.entries.Any(e => e.id == "form.worm"));
        }
        private static void ProjectionRoundTrips()
        {
            for (int heading = 0; heading < 360; heading += 15)
            {
                V2 local = new V2(4, -3), origin = new V2(120, -73);
                V2 world = Projection.InteriorToWorld(local, origin, heading);
                V2 roundtrip = Projection.WorldToInterior(world, origin, heading);
                Close(roundtrip.x, local.x); Close(roundtrip.y, local.y);
                V2 screen = Projection.ToScreen(world, 6), back = Projection.ToGround(screen, 6);
                Close(world.x, back.x); Close(world.y, back.y);
            }
        }
        private static void WormStart()
        { var s = Sim(); var p = Add(s, species: Species.Brood); Assert(p.IsWorm); Close(p.maxHealth, 160); Assert(s.Content.entries.Count(e => e.category == "hero") == 8); }
        private static void WormEvolution()
        {
            var s = Sim(); var p = Add(s, species: Species.Brood);
            Assert(!Do(s, p, CommandKind.Evolve, "base.molt").success);
            At(p, "base", 0, 4, -4); Assert(!Do(s, p, CommandKind.Evolve, "base.molt").success);
            p.inventory.Add("biomass", 8); p.inventory.Add("sample", 1);
            long seq = p.lastCommand + 1;
            var cmd = new GameCommand { sequence = seq, kind = CommandKind.Evolve, targetId = "base.molt" };
            Assert(s.Execute(p.id, cmd).success); Assert(!p.IsWorm); Close(p.maxHealth, 250);
            Assert(!s.Execute(p.id, cmd).success); Assert(p.inventory.Count("biomass") == 0 && p.inventory.Count("sample") == 0);
        }
        private static void WormPersistence()
        {
            var s = Sim(); var p = Add(s, species: Species.Brood); At(p, "base", 0, 4, -4);
            Assert(Do(s, p, CommandKind.SwitchSpecies, value: "Human").success);
            Assert(Do(s, p, CommandKind.SwitchSpecies, value: "Brood").success); Assert(p.IsWorm);
            var codec = new JsonCodec(); var restored = codec.Decode(codec.Encode(s.World)); Assert(restored.Player("a").IsWorm);
            p.broodForm = BroodForm.Adult; restored = codec.Decode(codec.Encode(s.World)); Assert(!restored.Player("a").IsWorm);
        }
        private static void SharedDriving()
        {
            foreach (var species in new[] { Species.Human, Species.Brood }) foreach (var id in new[] { "crawler", "beast" })
            { var s = Sim(); var p = Add(s, species: species); Seat(s, p, id + ".driver"); Assert(p.stationId == id + ".driver"); }
        }
        private static void StationRace()
        { var s = Sim(); var a = Add(s); var b = Add(s, "b"); Seat(s, a, "crawler.driver"); b.location = a.location.Copy(); Assert(!Do(s, b, CommandKind.Interact, "crawler.driver").success); }
        private static void WeaponOwnership()
        {
            var s = Sim(); var a = Add(s); var b = Add(s, "b", Species.Brood); var v = s.World.Vehicle("crawler");
            Seat(s, a, "crawler.driver"); Assert(s.CanControlGroup(a, v, 0)); Seat(s, b, "crawler.gunner");
            Assert(!s.CanControlGroup(a, v, 0)); Assert(s.CanControlGroup(a, v, 1)); Assert(s.CanControlGroup(b, v, 0)); Assert(!s.CanControlGroup(b, v, 1));
            Do(s, b, CommandKind.LeaveStation); Assert(s.CanControlGroup(a, v, 0));
        }
        private static void Disconnect()
        {
            var s = Sim(); var p = Add(s); Seat(s, p, "crawler.driver"); var v = s.World.Vehicle("crawler");
            v.speed = 8; v.cruise = true; p.input.fire = true; s.Disconnect(p.id);
            Close(v.speed, 0); Assert(!v.cruise && v.stations[0].playerId == "" && !p.input.fire);
        }
        private static void StaleInput()
        { var s = Sim(); var p = Add(s); s.SetInput(p.id, new PlayerInput { sequence = 1, fire = true, move = new V2(1, 0), aim = new V2(30, -9) }); Step(s, 20); Assert(!p.input.fire && p.input.move.Length == 0); }
        private static void MovingInterior()
        {
            var s = Sim(); var a = Add(s); var b = Add(s, "b", Species.Brood); Seat(s, a, "crawler.driver");
            At(b, "crawler.inside", 0, 2, 1); var v = s.World.Vehicle("crawler"); V2 start = v.position;
            for (int i = 0; i < 60; i++) { s.SetInput(a.id, new PlayerInput { sequence = i + 1, move = new V2(0, 1) }); s.Tick(GameSimulation.TickSeconds); }
            Assert(V2.Distance(v.position, start) > 1); Close(b.location.position.x, 2); Close(b.location.position.y, 1); Assert(b.location.floor == 0);
        }
        private static void ExitSafety()
        {
            var s = Sim(); var p = Add(s); var portal = s.World.portals.Find(x => x.id == "crawler.inside.exit"); p.location = portal.from.Copy();
            var v = s.World.Vehicle("crawler"); v.speed = 3; Assert(!s.ValidatePortal(p, portal).success);
            v.speed = 0; v.flightMode = FlightMode.Low; Assert(!s.ValidatePortal(p, portal).success);
        }
        private static void BlockedExit()
        {
            var s = Sim(); var p = Add(s); var portal = s.World.portals.Find(x => x.id == "crawler.inside.exit"); p.location = portal.from.Copy();
            V2 dest = s.PortalDestination(portal).position;
            s.World.Space("world").floors[0].walls.Add(new WallState { bounds = new Box2(dest, new V2(3, 3)) });
            Assert(!s.ValidatePortal(p, portal).success); Step(s, 60); Assert(p.location.spaceId == "crawler.inside");
        }
        private static void PortalCommit()
        {
            var s = Sim(); var p = Add(s); var driver = Add(s, "b"); Seat(s, driver, "crawler.driver");
            var portal = s.World.portals.Find(x => x.id == "crawler.inside.exit"); p.location = portal.from.Copy(); p.portalReadyAt = 0;
            s.Tick(GameSimulation.TickSeconds); Assert(p.transitionId != ""); s.World.Vehicle("crawler").speed = 4;
            Step(s, 10); Assert(p.location.spaceId == "crawler.inside");
        }
        private static void Stairs()
        {
            var s = Sim(); var p = Add(s); var stair = s.World.portals.First(x => x.stairs && x.from.spaceId == "lab");
            p.location = stair.from.Copy(); p.portalReadyAt = 0; Step(s, 30);
            Assert(p.location.spaceId == "lab" && p.location.floor == 1); Assert(p.transitionId == "");
        }
        private static void FloorCombat()
        {
            var s = Sim(); var p = Add(s); At(p, "lab", 0, 0, 0); s.World.enemies.Clear();
            var upper = new EnemyState { id = "upper", location = WorldFactory.At("lab", 1, new V2(3, 0)) }; s.World.enemies.Add(upper);
            s.SetInput(p.id, new PlayerInput { sequence = 1, fire = true, aim = new V2(3, 0) }); Step(s, 2); Close(upper.health, 100);
            s.World.Space("lab").floors[0].walls.Add(new WallState { bounds = new Box2(new V2(1.5f, 0), new V2(.3f, 4)) });
            upper.location.floor = 0; Step(s, 10); Close(upper.health, 100);
        }
        private static void WormCombat()
        {
            var s = Sim(); var p = Add(s, species: Species.Brood); At(p, "world", 0, 0, -20); s.World.enemies.Clear();
            var e = new EnemyState { id = "bite", location = WorldFactory.At("world", 0, new V2(1, -20)) }; s.World.enemies.Add(e);
            s.SetInput(p.id, new PlayerInput { sequence = 1, fire = true, aim = e.location.position }); Step(s, 1); Assert(e.health < 100);
            Assert(Do(s, p, CommandKind.Ability).success); float health = p.health; Step(s, 60); Close(p.health, health);
            At(p, "lab", 0, 0, 0); p.abilityReadyAt = 0; Assert(!Do(s, p, CommandKind.Ability).success);
        }
        private static void ModuleCompatibility()
        {
            var s = Sim(); var p = Add(s, species: Species.Brood); At(p, "crawler.inside", 0, -4, 2); var v = s.World.Vehicle("crawler");
            v.cargo.Add("module.biological.light", 1); int before = v.cargo.Total;
            Assert(!Do(s, p, CommandKind.InstallModule, "crawler", "biological.light", "primary").success); Assert(before == v.cargo.Total);
            var tooBig = s.Content.Module("mechanical.heavy"); s.World.research.Add("mechanical.weapons");
            Assert(!s.ValidateModule(v, v.hardpoints[1], tooBig).success);
        }
        private static void ModuleTransaction()
        {
            var s = Sim(); var p = Add(s, species: Species.Brood); At(p, "crawler.inside", 0, -4, 2); s.World.research.Add("mechanical.weapons"); var v = s.World.Vehicle("crawler");
            var cmd = new GameCommand { sequence = 1, kind = CommandKind.InstallModule, targetId = "crawler", value = "mechanical.heavy", slot = "primary" };
            int old = v.cargo.Count("module.mechanical.light"); Assert(s.Execute(p.id, cmd).success);
            Assert(v.cargo.Count("module.mechanical.heavy") == 0 && v.cargo.Count("module.mechanical.light") == old + 1);
            Assert(!s.Execute(p.id, cmd).success); Assert(v.cargo.Count("module.mechanical.light") == old + 1);
        }
        private static void InventoryTransactions()
        { var a = new Inventory { capacity = 20 }; var b = new Inventory { capacity = 5 }; a.Add("ore", 10); Assert(!Inventory.Transfer(a, b, "ore", -1)); Assert(!Inventory.Transfer(a, b, "ore", 6)); Assert(Inventory.Transfer(a, b, "ore", 5)); Assert(a.Count("ore") + b.Count("ore") == 10); }
        private static void Production()
        {
            var s = Sim(); var p = Add(s); At(p, "base", 0, -5, 2); Assert(Do(s, p, CommandKind.Craft, "base.workshop", "ammo").success);
            var inv = s.World.warehouse; int afterCost = inv.Count("metal"); inv.capacity = inv.Total;
            Step(s, 120); var f = s.World.facilities.Find(x => x.id == "base.workshop"); Assert(f.jobs.Count == 1 && f.jobs[0].remaining == 0);
            inv.capacity += 100; Step(s, 2); Assert(f.jobs.Count == 0 && inv.Count("ammo") == 60 && inv.Count("metal") == afterCost); Step(s, 20); Assert(inv.Count("ammo") == 60);
        }
        private static void OfflinePause()
        { var s = Sim(); var p = Add(s); s.Disconnect(p.id); double t = s.World.time; Step(s, 100); Assert(s.World.time == t); }
        private static void Research()
        {
            var s = Sim(); var p = Add(s); Assert(!Do(s, p, CommandKind.Research, "lab.research").success);
            At(p, "lab", 1, -4, 2); Assert(!Do(s, p, CommandKind.Research, "lab.research").success);
            At(p, "lab", 0, -5, 2); Assert(Do(s, p, CommandKind.Interact, "lab.power").success);
            At(p, "lab", 1, -4, 2); Assert(Do(s, p, CommandKind.Research, "lab.research").success); Assert(s.World.research.Contains("mechanical.weapons"));
        }
        private static void Building()
        {
            var s = Sim(); var p = Add(s); At(p, "base", 0, 0, -4);
            var cmd = new GameCommand { sequence = 1, kind = CommandKind.Build, piece = PieceKind.Stairs, floor = 0, position = new V2(0, -2) };
            Assert(s.Execute(p.id, cmd).success); Assert(s.CanStand(WorldFactory.At("base", 1, new V2(0, 2))));
            At(p, "base", 0, 0, -2); p.portalReadyAt = 0; Step(s, 30); Assert(p.location.floor == 1);
            cmd.sequence = 2; cmd.floor = 4; Assert(!s.Execute(p.id, cmd).success);
        }
        private static void EscapeRoutes()
        {
            var s = Sim(); var p = Add(s); At(p, "base", 0, 0, 0);
            for (int x = -2; x <= 2; x++) for (int y = -2; y <= 2; y++)
                if ((Math.Abs(x) == 2 || Math.Abs(y) == 2) && !(x == 0 && y == -2))
                    s.World.pieces.Add(new BuildPiece { id = s.World.NewId("wall"), kind = PieceKind.Wall, location = WorldFactory.At("base", 0, new V2(x, y)), rotation = Math.Abs(x) == 2 ? 1 : 0 });
            var result = s.Execute(p.id, new GameCommand { sequence = 1, kind = CommandKind.Build, piece = PieceKind.Wall, position = new V2(0, -2) });
            Assert(!result.success, "enclosure was permitted");
        }
        private static void Backups()
        {
            string dir = Path.Combine(Path.GetTempPath(), "ashfall-spec-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(dir);
            try
            {
                string path = Path.Combine(dir, "world.sav"); var store = new SaveStore(path, new JsonCodec()); var s = Sim(); var p = Add(s, species: Species.Brood);
                store.Save(s.World); p.broodForm = BroodForm.Adult; s.World.tick = 1; store.Save(s.World); s.World.tick = 2; store.Save(s.World);
                File.WriteAllText(path, "corrupted"); var recovered = store.Load(); Assert(recovered.tick == 1 && recovered.Player("a").broodForm == BroodForm.Adult); Assert(store.LastLoadedPath.EndsWith(".bak1"));
                recovered.tick = 3; store.Save(recovered); Assert(store.Load().tick == 3); Assert(File.Exists(path + ".bak2"));
            }
            finally { RemoveTestDirectory(dir); }
        }
        private static void InvalidSave()
        {
            string dir = Path.Combine(Path.GetTempPath(), "ashfall-spec-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(dir);
            try
            {
                string path = Path.Combine(dir, "world.sav"); var store = new SaveStore(path, new JsonCodec()); var s = Sim(); store.Save(s.World);
                string before = File.ReadAllText(path); s.World.warehouse.items[0].amount = -1;
                bool rejected = false; try { store.Save(s.World); } catch (InvalidDataException) { rejected = true; }
                Assert(rejected && before == File.ReadAllText(path));
            }
            finally { RemoveTestDirectory(dir); }
        }
        private static void RestoreSeat()
        {
            var s = Sim(); var a = Add(s); Seat(s, a, "crawler.driver"); s.PrepareRestoredWorld(); var b = Add(s, "b"); Seat(s, b, "crawler.driver");
            a = s.Connect("a", "a", Species.Human); Assert(a != null && a.stationId == "" && a.location.spaceId == "crawler.inside");
        }
        private static void RemoveTestDirectory(string directory)
        {
            string full = Path.GetFullPath(directory);
            string root = Path.GetFullPath(Path.GetTempPath());
            if (!full.StartsWith(Path.Combine(root, "ashfall-spec-"), StringComparison.OrdinalIgnoreCase)
                || Path.GetDirectoryName(full) != root.TrimEnd(Path.DirectorySeparatorChar))
                throw new IOException("Refusing to remove an unexpected test path.");
            Directory.Delete(full, true);
        }
        private static void FourPlayers() { var s = Sim(); for (int i = 0; i < 4; i++) Assert(Add(s, "p" + i) != null); Assert(Add(s, "fifth") == null); }
        private static void BadInput()
        { var s = Sim(); var p = Add(s); Assert(!s.SetInput(p.id, new PlayerInput { sequence = 1, move = new V2(float.NaN, 0) })); Step(s, 2); Assert(p.location.position.Finite); }
        private static void Soak()
        {
            var s = Sim(); var players = Enumerable.Range(0, 4).Select(i => Add(s, "p" + i, i % 2 == 0 ? Species.Human : Species.Brood)).ToArray();
            Seat(s, players[0], "crawler.driver"); Seat(s, players[1], "crawler.gunner");
            At(players[2], "crawler.inside", 0, 0, 0); At(players[3], "lab", 1, 0, 0);
            for (int i = 0; i < 30 * 60 * 15; i++)
            {
                s.SetInput(players[0].id, new PlayerInput { sequence = i + 1, move = new V2((i / 240) % 2 == 0 ? .1f : -.1f, .4f), aim = new V2(80, -20) });
                s.SetInput(players[1].id, new PlayerInput { sequence = i + 1, fire = i % 3 == 0, aim = new V2(60, -20) });
                s.Tick(GameSimulation.TickSeconds);
            }
            Assert(s.World.tick == 27000); Assert(players.All(p => p.location.position.Finite));
            Assert(players[2].location.spaceId == "crawler.inside" && players[3].location.floor == 1);
            SaveStore.Validate(s.World);
        }
    }
}