Tests: mined lots, stack merge and split by lot, save GC roots, rebuilt index, old saves read Lot 0
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
df67c875e3
commit
7d9c4d4844
2 changed files with 313 additions and 5 deletions
|
|
@ -23,11 +23,11 @@ public sealed class MiningCut5aTests : IDisposable
|
|||
_scenes.Dispose();
|
||||
}
|
||||
|
||||
private static SimpleCommodityData Commodity(string name, int maxStack = 100) =>
|
||||
internal static SimpleCommodityData Commodity(string name, int maxStack = 100) =>
|
||||
new SimpleCommodityData { Name = name, Shape = new Shape(), Price = 5, MaxStack = maxStack };
|
||||
|
||||
// Registers the ores and the kind's yields in the scene's catalog (the kind refs need the stored commodities).
|
||||
private static void Stock(MiningCut3Tests.Scene scene, FieldKindData kind, params (SimpleCommodityData Ore, float Abundance)[] yields)
|
||||
internal static void Stock(MiningCut3Tests.Scene scene, FieldKindData kind, params (SimpleCommodityData Ore, float Abundance)[] yields)
|
||||
{
|
||||
kind.Yields.Clear();
|
||||
foreach (var (ore, abundance) in yields)
|
||||
|
|
@ -38,7 +38,7 @@ public sealed class MiningCut5aTests : IDisposable
|
|||
scene.Cache.Upsert(kind);
|
||||
}
|
||||
|
||||
private EquippedCargoBay Bay(MiningCut3Tests.Scene scene, Ship ship, int width, int height, string name)
|
||||
internal static EquippedCargoBay Bay(MiningCut3Tests.Scene scene, Ship ship, int width, int height, string name)
|
||||
{
|
||||
var shape = new Shape(width, height);
|
||||
foreach (var cell in shape.AllCoordinates) shape[cell] = true;
|
||||
|
|
@ -49,10 +49,10 @@ public sealed class MiningCut5aTests : IDisposable
|
|||
return bay;
|
||||
}
|
||||
|
||||
private static int Units(IEnumerable<ItemInstance> items, string ore, MiningCut3Tests.Scene scene) =>
|
||||
internal static int Units(IEnumerable<ItemInstance> items, string ore, MiningCut3Tests.Scene scene) =>
|
||||
items.OfType<SimpleCommodity>().Where(c => scene.Items.GetData(c).Name == ore).Sum(c => c.Quantity);
|
||||
|
||||
private static int Units(EquippedCargoBay bay, string ore, MiningCut3Tests.Scene scene) => Units(bay.Cargo.Keys, ore, scene);
|
||||
internal static int Units(EquippedCargoBay bay, string ore, MiningCut3Tests.Scene scene) => Units(bay.Cargo.Keys, ore, scene);
|
||||
|
||||
// A rig whose rock (size .5: 4 units, whole) yields `ores`, shooter holding one bay.
|
||||
private (MiningCut4bTests.Rig Rig, EquippedCargoBay Bay) Mining(int bayWidth = 4, int bayHeight = 4, float damage = 1000f,
|
||||
|
|
|
|||
308
tests/Aetheria.Shared.Tests/MiningCut5bTests.cs
Normal file
308
tests/Aetheria.Shared.Tests/MiningCut5bTests.cs
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using CultMath;
|
||||
using GameCult.Caching;
|
||||
using GameCult.Caching.MessagePack;
|
||||
using MessagePack;
|
||||
using Xunit;
|
||||
|
||||
// Mining Cut 5b (docs/mining-cut-refresh.md, Cut 5, Q6b = A): a mined unit carries an Extracted lot naming the zone, the belt
|
||||
// and the ore; stacks merge, split, restore and save by lot. Fixtures are Cut 3's belt scene and Cut 5a's bay and stock helpers.
|
||||
public sealed class MiningCut5bTests : IDisposable
|
||||
{
|
||||
private readonly MiningCut3Tests _scenes = new MiningCut3Tests();
|
||||
private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-mining5b-" + Guid.NewGuid().ToString("N"));
|
||||
private uint _ordinal;
|
||||
|
||||
public MiningCut5bTests() => Directory.CreateDirectory(_root);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_scenes.Dispose();
|
||||
Directory.Delete(_root, true);
|
||||
}
|
||||
|
||||
private sealed class Field
|
||||
{
|
||||
public MiningCut3Tests.Scene Scene;
|
||||
public Galaxy Galaxy;
|
||||
public SimpleCommodityData Ore;
|
||||
}
|
||||
|
||||
// Two belts of rocks (size .5: 4 whole units a rock), one ore, in a galaxy of three zones.
|
||||
private Field Build()
|
||||
{
|
||||
var ore = MiningCut5aTests.Commodity("Rock");
|
||||
var kind = MiningCut3Tests.Kind("Asteroid", 4f);
|
||||
var scene = _scenes.BuildScene(new[] { kind },
|
||||
MiningCut3Tests.Belt(kind, MiningCut3Tests.Rock(150f, 0f), MiningCut3Tests.Rock(155f, .3f)),
|
||||
MiningCut3Tests.Belt(kind, MiningCut3Tests.Rock(160f, .6f)));
|
||||
MiningCut5aTests.Stock(scene, kind, (ore, 1f));
|
||||
var game = new SavedGame
|
||||
{
|
||||
Seed = 5, Factions = new CultRecordRef<Faction>[0], Relationships = new FactionRelationship[0],
|
||||
HomeZones = new Dictionary<int, int>(), BossZones = new Dictionary<int, int>(), DiscoveredZones = new[] { 0 },
|
||||
ActionBarBindings = new SavedActionBarBinding[0], Entrance = 0, Exit = -1
|
||||
};
|
||||
var zones = Enumerable.Range(0, 3)
|
||||
.Select(i => new SavedZone { Name = $"Z{i}", AdjacentZones = new int[0], Factions = new int[0], Owner = -1 }).ToArray();
|
||||
RunSave.Commit(scene.Cache, game, zones, new ProvenanceLedger());
|
||||
return new Field { Scene = scene, Galaxy = new Galaxy(scene.Cache, scene.Cache.GetGlobal<SavedGame>(), _ => { }), Ore = ore };
|
||||
}
|
||||
|
||||
private static Zone ZoneOf(Field f, int index)
|
||||
{
|
||||
var settings = MiningCut3Tests.PlanetSettings();
|
||||
settings.AsteroidYield = new ExponentialLerp { Minimum = 4f, Maximum = 4f, Exponent = 1f };
|
||||
return index < 0
|
||||
? new Zone(f.Scene.Items, settings, f.Scene.Pack, new GalaxyZone { Name = "Lone", Owner = null }, null)
|
||||
: new Zone(f.Scene.Items, settings, f.Scene.Pack, f.Galaxy.Zones[index], f.Galaxy);
|
||||
}
|
||||
|
||||
private Ship Shooter(Field f, Zone zone)
|
||||
{
|
||||
var ship = new Ship(f.Scene.Items, zone, MiningCut3Tests.Mint(f.Scene, f.Scene.Hull), new EntitySettings());
|
||||
zone.Admit(ship, piloted: false);
|
||||
return ship;
|
||||
}
|
||||
|
||||
private void Mine(Field f, Zone zone, Ship ship, int belt, int rock)
|
||||
{
|
||||
var chunk = new ChunkId(f.Scene.Belts[belt], rock);
|
||||
FireControl.Extract(zone, chunk, zone.Wear(chunk, 1e6f), ship, SimulationDice.For(zone.DropSeed, SimulationDice.OreStream, ++_ordinal));
|
||||
}
|
||||
|
||||
private static SimpleCommodity[] Stacks(EquippedCargoBay bay) => bay.Cargo.Keys.OfType<SimpleCommodity>().OrderBy(s => s.Lot).ToArray();
|
||||
|
||||
[Fact]
|
||||
public void MinedUnitsCarryAnExtractedLotNamingZoneBeltAndOre()
|
||||
{
|
||||
var f = Build();
|
||||
var zone = ZoneOf(f, 2);
|
||||
var ship = Shooter(f, zone);
|
||||
var bay = MiningCut5aTests.Bay(f.Scene, ship, 4, 4, "Hold");
|
||||
Mine(f, zone, ship, 1, 0);
|
||||
|
||||
var stack = Assert.Single(Stacks(bay));
|
||||
Assert.Equal(4, stack.Quantity);
|
||||
Assert.NotEqual(0, stack.Lot);
|
||||
var lot = f.Scene.Items.GetLot(stack);
|
||||
var origin = Assert.IsType<Extracted>(lot.Origin);
|
||||
Assert.Equal(2, origin.Zone);
|
||||
Assert.Equal(f.Scene.Belts[1], origin.Body.Key);
|
||||
Assert.Equal(stack.Data.Key, origin.Commodity.Key);
|
||||
Assert.Equal(stack.Data.Key, lot.Design.Key);
|
||||
Assert.Equal(1f, lot.Quality);
|
||||
Assert.Equal(default, lot.Product);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AZoneWithNoGalaxyIsMinus1()
|
||||
{
|
||||
var f = Build();
|
||||
var zone = ZoneOf(f, -1);
|
||||
var ship = Shooter(f, zone);
|
||||
var bay = MiningCut5aTests.Bay(f.Scene, ship, 4, 4, "Hold");
|
||||
Mine(f, zone, ship, 0, 0);
|
||||
Assert.Equal(-1, Assert.IsType<Extracted>(f.Scene.Items.GetLot(Assert.Single(Stacks(bay))).Origin).Zone);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnitsFromOneSourceStack_UnitsFromTwoSourcesDoNot()
|
||||
{
|
||||
var f = Build();
|
||||
var zone = ZoneOf(f, 1);
|
||||
var ship = Shooter(f, zone);
|
||||
var bay = MiningCut5aTests.Bay(f.Scene, ship, 4, 4, "Hold");
|
||||
Mine(f, zone, ship, 0, 0);
|
||||
Mine(f, zone, ship, 0, 1);
|
||||
var one = Assert.Single(Stacks(bay));
|
||||
Assert.Equal(8, one.Quantity);
|
||||
|
||||
Mine(f, zone, ship, 1, 0);
|
||||
var stacks = Stacks(bay);
|
||||
Assert.Equal(new[] { 8, 4 }, stacks.Select(s => s.Quantity));
|
||||
Assert.NotEqual(stacks[0].Lot, stacks[1].Lot);
|
||||
Assert.Equal(2, f.Scene.Items.Lots.Lots.Values.Count(l => l.Origin is Extracted));
|
||||
// Two lots of one ore still total one commodity for the market and the hold.
|
||||
Assert.Equal(12, MiningCut5aTests.Units(bay, "Rock", f.Scene));
|
||||
}
|
||||
|
||||
// TryFindSpace and the at-coordinate store both look for a stack to top up: a stack of another lot is not one.
|
||||
[Fact]
|
||||
public void ASearchAndAStoreIgnoreAStackOfAnotherLot()
|
||||
{
|
||||
var f = Build();
|
||||
var zone = ZoneOf(f, 0);
|
||||
var ship = Shooter(f, zone);
|
||||
var bay = MiningCut5aTests.Bay(f.Scene, ship, 1, 1, "Hold");
|
||||
var key = new CultRecordRef<ItemData>(f.Scene.Cache.RefOf(f.Ore).Key);
|
||||
SimpleCommodity Units(int lot, int quantity) => new SimpleCommodity { Data = key, Lot = lot, Quantity = quantity };
|
||||
|
||||
Assert.True(bay.TryStore(Units(7, 90)));
|
||||
Assert.False(bay.TryFindSpace(Units(8, 5), out _));
|
||||
Assert.True(bay.TryFindSpace(Units(7, 5), out _));
|
||||
var other = Units(8, 5);
|
||||
Assert.False(bay.TryStore(other, new int2(0, 0)));
|
||||
Assert.Equal(90, Assert.Single(bay.Cargo.Keys.OfType<SimpleCommodity>()).Quantity);
|
||||
Assert.Equal(5, other.Quantity);
|
||||
Assert.True(bay.TryStore(Units(7, 5), new int2(0, 0)));
|
||||
Assert.Equal(95, Assert.Single(bay.Cargo.Keys.OfType<SimpleCommodity>()).Quantity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ASplitStackKeepsItsLot()
|
||||
{
|
||||
var f = Build();
|
||||
var zone = ZoneOf(f, 0);
|
||||
var ship = Shooter(f, zone);
|
||||
var bay = MiningCut5aTests.Bay(f.Scene, ship, 4, 4, "Hold");
|
||||
Mine(f, zone, ship, 0, 0);
|
||||
var stack = Assert.Single(Stacks(bay));
|
||||
|
||||
var part = bay.Remove(stack, 3);
|
||||
Assert.Equal(3, part.Quantity);
|
||||
Assert.Equal(stack.Lot, part.Lot);
|
||||
Assert.NotEqual(0, part.Lot);
|
||||
Assert.Equal(1, stack.Quantity);
|
||||
}
|
||||
|
||||
// A saved stack above its design's MaxStack comes back as several stacks, every one of the saved lot.
|
||||
[Fact]
|
||||
public void ARestoredOversizeStackKeepsItsLot()
|
||||
{
|
||||
var f = Build();
|
||||
var zone = ZoneOf(f, 0);
|
||||
var ship = Shooter(f, zone);
|
||||
var bay = MiningCut5aTests.Bay(f.Scene, ship, 4, 4, "Hold");
|
||||
for (var rock = 0; rock < 2; rock++) Mine(f, zone, ship, 0, rock);
|
||||
var lot = Assert.Single(Stacks(bay)).Lot;
|
||||
|
||||
var pack = EntitySerializer.Pack(ship);
|
||||
f.Ore.MaxStack = 3;
|
||||
var restored = EntitySerializer.Unpack(f.Scene.Items, zone, pack);
|
||||
|
||||
var stacks = restored.CargoBays.SelectMany(Stacks).ToArray();
|
||||
Assert.Equal(8, stacks.Sum(s => s.Quantity));
|
||||
Assert.True(stacks.Length >= 3);
|
||||
Assert.All(stacks, s => Assert.Equal(lot, s.Lot));
|
||||
}
|
||||
|
||||
private CultCache Open() => AetheriaStores.Open(Path.Combine(_root, "Aetheria.cc"), Path.Combine(_root, "run.cc"), Path.Combine(_root, "player.cc"));
|
||||
|
||||
private static ShipPack PackHolding(params ItemInstance[] stacks) => new ShipPack
|
||||
{
|
||||
Name = "ship", Hull = new EquippableItem(),
|
||||
Equipment = Array.Empty<(int2, EquippableItem)>(), CargoBays = Array.Empty<(int2, EquippableItem)>(),
|
||||
DockingBays = Array.Empty<(int2, EquippableItem)>(),
|
||||
CargoContents = new[] { stacks.Select((s, i) => (new int2(i, 0), s)).ToArray() },
|
||||
DockingBayContents = Array.Empty<(int2, ItemInstance)[]>(), Children = Array.Empty<EntityPack>(),
|
||||
Temperature = new float[0, 0], Armor = new float[0, 0], Conductivity = new bool2[0, 0],
|
||||
DockingBayAssignments = Array.Empty<int>(), Settings = new EntitySettings(), WeaponGroups = Array.Empty<int[]>()
|
||||
};
|
||||
|
||||
// The ledger as the run file holds it after a save of ships holding `held`.
|
||||
private ProvenanceLedger SaveAndReload(ProvenanceLedger live, params ItemInstance[] held)
|
||||
{
|
||||
using (var cache = AetheriaStores.Open(Path.Combine(_root, "Aetheria.cc"), catalogWritable: true))
|
||||
{
|
||||
cache.Commit(batch => batch.Upsert(new VerseGrammar { Revision = 1 }));
|
||||
}
|
||||
using (var cache = Open())
|
||||
{
|
||||
var game = new SavedGame
|
||||
{
|
||||
Factions = new CultRecordRef<Faction>[0], Relationships = new FactionRelationship[0],
|
||||
HomeZones = new Dictionary<int, int>(), BossZones = new Dictionary<int, int>(), DiscoveredZones = new[] { 0 },
|
||||
ActionBarBindings = new SavedActionBarBinding[0], Exit = -1
|
||||
};
|
||||
var zone = new SavedZone
|
||||
{
|
||||
Name = "Zone 0", AdjacentZones = Array.Empty<int>(), Factions = Array.Empty<int>(), Owner = -1,
|
||||
Contents = new ZonePack { Entities = new List<EntityPack> { PackHolding(held) } }
|
||||
};
|
||||
RunSave.Commit(cache, game, new[] { zone }, live);
|
||||
}
|
||||
using (var cache = Open()) return RunSave.Lots(cache);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AMinedStackSurvivesSaveGc()
|
||||
{
|
||||
var f = Build();
|
||||
var zone = ZoneOf(f, 1);
|
||||
var ship = Shooter(f, zone);
|
||||
var bay = MiningCut5aTests.Bay(f.Scene, ship, 4, 4, "Hold");
|
||||
Mine(f, zone, ship, 0, 0);
|
||||
Mine(f, zone, ship, 1, 0);
|
||||
var held = Stacks(bay);
|
||||
Assert.Equal(2, held.Length);
|
||||
// A third lot nobody holds: floated and gone, or never collected.
|
||||
var orphan = f.Scene.Items.ExtractedLot(1, new CultRecordKey("elsewhere"), f.Ore);
|
||||
|
||||
var saved = SaveAndReload(f.Scene.Items.Lots, held);
|
||||
|
||||
Assert.Equal(held.Select(s => s.Lot).OrderBy(i => i), saved.Lots.Keys.OrderBy(i => i));
|
||||
Assert.DoesNotContain(orphan, saved.Lots.Keys);
|
||||
var origin = Assert.IsType<Extracted>(saved[held[0].Lot].Origin);
|
||||
Assert.Equal(1, origin.Zone);
|
||||
Assert.Equal(f.Scene.Belts[0], origin.Body.Key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheLotIndexIsRebuiltOnContinue()
|
||||
{
|
||||
var f = Build();
|
||||
var zone = ZoneOf(f, 1);
|
||||
var ship = Shooter(f, zone);
|
||||
var bay = MiningCut5aTests.Bay(f.Scene, ship, 4, 4, "Hold");
|
||||
Mine(f, zone, ship, 0, 0);
|
||||
var held = Assert.Single(Stacks(bay));
|
||||
|
||||
var saved = SaveAndReload(f.Scene.Items.Lots, held);
|
||||
var continued = new ItemManager(1u, f.Scene.Cache, saved, RunSaveTests.TestSettings(), _ => { });
|
||||
|
||||
Assert.Equal(held.Lot, continued.ExtractedLot(1, f.Scene.Belts[0], f.Ore));
|
||||
Assert.Single(saved.Lots);
|
||||
var next = continued.ExtractedLot(1, f.Scene.Belts[1], f.Ore);
|
||||
Assert.NotEqual(held.Lot, next);
|
||||
Assert.Equal(2, saved.Lots.Count);
|
||||
// Zone and ore tell lots apart as the belt does.
|
||||
Assert.NotEqual(next, continued.ExtractedLot(2, f.Scene.Belts[1], f.Ore));
|
||||
}
|
||||
|
||||
// A save from before the field reached SimpleCommodity: Data, Rotation, Quantity and nothing else.
|
||||
[Fact]
|
||||
public void OldSavesReadLotZero()
|
||||
{
|
||||
var f = Build();
|
||||
var options = CultDocumentMessagePackSerialization.OptionsFor(typeof(ItemInstance).Assembly);
|
||||
var key = new CultRecordRef<ItemData>(f.Scene.Cache.RefOf(f.Ore).Key);
|
||||
var buffer = new System.Buffers.ArrayBufferWriter<byte>();
|
||||
var writer = new MessagePackWriter(buffer);
|
||||
writer.WriteArrayHeader(2);
|
||||
writer.Write(0); // Union(0, SimpleCommodity)
|
||||
writer.WriteArrayHeader(3);
|
||||
MessagePackSerializer.Serialize(ref writer, key, options);
|
||||
MessagePackSerializer.Serialize(ref writer, ItemRotation.None, options);
|
||||
writer.Write(7);
|
||||
writer.Flush();
|
||||
|
||||
var old = Assert.IsType<SimpleCommodity>(MessagePackSerializer.Deserialize<ItemInstance>(buffer.WrittenMemory, options));
|
||||
Assert.Equal(7, old.Quantity);
|
||||
Assert.Equal(0, old.Lot);
|
||||
Assert.Equal(key.Key, old.Data.Key);
|
||||
|
||||
// A lot rides the wire; a Lot-0 stack is no root, so the old stack saves no lot.
|
||||
var back = MessagePackSerializer.Deserialize<ItemInstance>(MessagePackSerializer.Serialize<ItemInstance>(
|
||||
new SimpleCommodity { Data = key, Quantity = 2, Lot = 9 }, options), options);
|
||||
Assert.Equal(9, back.Lot);
|
||||
Assert.Empty(SaveAndReload(new ProvenanceLedger(), old).Lots);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue