From c5ff7685e5b3ac5dc46fce74ef1b302de09af1e6 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Sun, 20 Sep 2026 21:16:49 +0800 Subject: feat(desktop): add Chinese village presentation prototype [Change Nature] - This commit adds a deterministic presentation-model feature, not MonoGame rendering. [New Capability] - Provide an immutable Jiangnan village scene with homes, waterways, paths, bridge, bamboo, and garden elements. - Provide typed home transitions, four-way input mapping, tick animation, NPC styles, and Chinese stage-one labels. [Implementation] - Add the pure VillagePresentation module and six focused MSTest cases. - Keep the model free of mutable global state and update only the required F# compile order. [Impact] - This commit affects the Desktop presentation and test projects only. - Kernel behavior, Game.fs wiring, MonoGame drawing, and existing acceptance artifacts remain unchanged. --- .../LivingVillage.Desktop.Tests.fsproj | 1 + src/LivingVillage.Desktop.Tests/PrototypeTests.fs | 102 +++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 src/LivingVillage.Desktop.Tests/PrototypeTests.fs (limited to 'src/LivingVillage.Desktop.Tests') diff --git a/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj b/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj index 405e8b5..569190a 100644 --- a/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj +++ b/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj @@ -9,6 +9,7 @@ + diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs new file mode 100644 index 0000000..b959d70 --- /dev/null +++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs @@ -0,0 +1,102 @@ +namespace LivingVillage.Desktop.Tests + +open Microsoft.VisualStudio.TestTools.UnitTesting +open LivingVillage.Kernel.Sim +open LivingVillage.Desktop.VillagePresentation + +[] +type PrototypeTests () = + + [] + member _.SceneContainsTheJiangnanPrototypeAndReachableHomeDoor () = + let first = createScene () + let second = createScene () + let requiredProps = + [ WhiteWallDarkTileHome + HomeDoor + RiverChannel + RiverBank + StoneBridge + StonePaving + Bamboo + VegetableGarden ] + + Assert.AreEqual(first, second) + Assert.AreEqual(scene, first) + Assert.IsTrue(first.Homes.Length > 0, "The prototype needs at least one home.") + Assert.IsTrue( + requiredProps + |> List.forall (fun prop -> first.Elements |> List.exists (fun element -> element.Prop = prop)), + "The scene is missing one or more required Jiangnan elements.") + + let home = first.Homes |> List.head + Assert.IsTrue(isNearHomeDoor first.InitialPlayerPosition home) + + [] + member _.HomeEntryAndExitExposeExplicitSuccessAndFailureBranches () = + let scene = createScene () + let home = scene.Homes |> List.head + let entered = enterHome scene.InitialPlayerPosition Outside + + match entered with + | Ok (Inside enteredHome) -> + Assert.AreEqual(home.Id, enteredHome) + | Ok Outside -> Assert.Fail("Entering a home must produce Inside state.") + | Error failure -> Assert.Fail(sprintf "Expected entry to succeed, got %A." failure) + + match entered with + | Ok inside -> + match enterHome scene.InitialPlayerPosition inside with + | Error (AlreadyInsideHome homeId) -> Assert.AreEqual(home.Id, homeId) + | other -> Assert.Fail(sprintf "Expected AlreadyInsideHome, got %A." other) + | Error _ -> Assert.Fail("The first entry must succeed before checking the duplicate entry.") + + match exitHome (Inside home.Id) with + | Ok Outside -> () + | other -> Assert.Fail(sprintf "Expected exit to succeed, got %A." other) + + match exitHome Outside with + | Error NotInsideHome -> () + | other -> Assert.Fail(sprintf "Expected NotInsideHome, got %A." other) + + match exitHome (Inside(HomeId 999)) with + | Error(HomeNotFound(HomeId 999)) -> () + | other -> Assert.Fail(sprintf "Expected HomeNotFound, got %A." other) + + let farAway = { X = scene.InitialPlayerPosition.X + 20; Y = scene.InitialPlayerPosition.Y + 20 } + match enterHome farAway Outside with + | Error NotNearHomeDoor -> () + | other -> Assert.Fail(sprintf "Expected NotNearHomeDoor, got %A." other) + + [] + member _.FourWayInputMapsToFourDirections () = + Assert.AreEqual(Some North, directionForInput UpInput) + Assert.AreEqual(Some South, directionForInput DownInput) + Assert.AreEqual(Some West, directionForInput LeftInput) + Assert.AreEqual(Some East, directionForInput RightInput) + Assert.AreEqual(None, directionForInput NoInput) + + [] + member _.AnimationFrameChangesWithSimulationTick () = + let first = animationFrame (SimulationTick 0L) + let second = animationFrame (SimulationTick 4L) + let nextCycle = animationFrame (SimulationTick 8L) + + Assert.AreNotEqual(first, second) + Assert.AreEqual(first, nextCycle) + + [] + member _.NpcIdProducesStableAndDistinctCharacterStyles () = + let first = characterStyle (NpcId 0) + let sameNpc = characterStyle (NpcId 0) + let second = characterStyle (NpcId 1) + + Assert.AreEqual(first, sameNpc) + Assert.AreNotEqual(first, second) + + [] + member _.StageOneCopyProvidesChineseInteractionLabels () = + Assert.AreEqual("互动", stageOneCopy.Interact) + Assert.AreEqual("需求", stageOneCopy.Needs) + Assert.AreEqual("交易", stageOneCopy.Trade) + Assert.AreEqual("进入民居", stageOneCopy.EnterHome) -- cgit v1.2.3