namespace LivingVillage.Desktop.Tests open Microsoft.VisualStudio.TestTools.UnitTesting open LivingVillage.Kernel open LivingVillage.Desktop /// P20 第一步回归:确定性生成器 + 尺寸参数化 + 30 spawn 核心可达 + 视口裁剪。 [] type MapGenTests () = let tilesEqual (a: MapGen.Result) (b: MapGen.Result) (message: string) = if a.Tiles.Length <> b.Tiles.Length then Assert.Fail(sprintf "%s: tile count %d <> %d" message a.Tiles.Length b.Tiles.Length) for i in 0 .. a.Tiles.Length - 1 do if a.Tiles.[i] <> b.Tiles.[i] then Assert.Fail(sprintf "%s: tile %d = %d, expected %d" message i a.Tiles.[i] b.Tiles.[i]) let checksum (map: MapGen.Result) : int64 = map.Tiles |> Array.mapi (fun i v -> int64 i * int64 (v + 1)) |> Array.sum let waterRunsInColumn (map: MapGen.Result) (x: int) : int list = let runs = ResizeArray() let mutable run = 0 for y in 0 .. map.Height - 1 do if map.Tiles.[y * map.Width + x] = int MapGen.GroundTile.Water then run <- run + 1 elif run > 0 then runs.Add run run <- 0 if run > 0 then runs.Add run List.ofSeq runs let waterNeighbourCount (map: MapGen.Result) (x: int) (y: int) : int = [ (1, 0); (-1, 0); (0, 1); (0, -1) ] |> List.sumBy (fun (dx, dy) -> let nx, ny = x + dx, y + dy if nx >= 0 && nx < map.Width && ny >= 0 && ny < map.Height && map.Tiles.[ny * map.Width + nx] = int MapGen.GroundTile.Water then 1 else 0) /// P30:把民居按「足迹邻近(两轴间隙 <=2 格)」聚成组团,返回各组规模。 let houseGroups (houses: MapGen.Building list) : int list = let arr = houses |> List.sortBy (fun b -> b.Top, b.Left) |> Array.ofList let n = arr.Length let parent = Array.init n id let rec find i = if parent.[i] = i then i else (parent.[i] <- find parent.[i]; parent.[i]) let union a b = let ra = find a let rb = find b if ra <> rb then parent.[ra] <- rb let near (a: MapGen.Building) (b: MapGen.Building) = let xGap = max (a.Left - (b.Left + b.Width - 1)) (b.Left - (a.Left + a.Width - 1)) let yGap = max (a.Top - (b.Top + b.Height - 1)) (b.Top - (a.Top + a.Height - 1)) xGap <= 2 && yGap <= 2 for i in 0 .. n - 1 do for j in i + 1 .. n - 1 do if near arr.[i] arr.[j] then union i j [ for i in 0 .. n - 1 -> find i ] |> List.groupBy id |> List.map (fun (_, group) -> group.Length) [] member _.SameSeedGeneratesByteIdenticalTerrain () = let a = MapGen.generateWithSize 64 48 (uint64 4242) let b = MapGen.generateWithSize 64 48 (uint64 4242) tilesEqual a b "64x48 same seed" Assert.AreEqual(a.Seed, b.Seed) Assert.AreEqual(checksum a, checksum b) let large1 = MapGen.generateWithSize 256 192 (uint64 7) let large2 = MapGen.generateWithSize 256 192 (uint64 7) tilesEqual large1 large2 "256x192 same seed" Assert.AreEqual(checksum large1, checksum large2) [] member _.DifferentSeedsProduceDifferentTerrain () = let a = MapGen.generateWithSize 256 192 (uint64 1) let b = MapGen.generateWithSize 256 192 (uint64 2) Assert.AreNotEqual(checksum a, checksum b, "different seeds must differ") [] member _.LargeMapThirtySpawnsAllReachCore () = let map = MapGen.generateWithSize 256 192 (uint64 4242) Assert.AreEqual(256, map.Width) Assert.AreEqual(192, map.Height) Assert.AreEqual(30, map.Spawns.Length) Assert.IsTrue(map.ReachabilityOk, "all 30 spawns must reach the core") Assert.IsTrue(map.ReachableTiles > 30, "core region must be walkable") for (x, y) in map.Spawns do Assert.AreNotEqual(int MapGen.GroundTile.Water, map.Tiles.[y * map.Width + x], sprintf "spawn (%d,%d) must be walkable" x y) [] member _.DefaultSizeMatchesLegacyBounds () = let map = MapGen.generate (MapGen.defaultParams (uint64 4242)) Assert.AreEqual(64, map.Width) Assert.AreEqual(48, map.Height) Assert.AreEqual(64 * 48, map.Tiles.Length) Assert.IsTrue(map.ReachabilityOk) Assert.AreEqual(30, map.Spawns.Length) [] member _.VisibleRangeCullsLargeMapToViewport () = let total = 256 * 192 let x0, y0, x1, y1 = MapGen.visibleTileRange 256 192 1000 1000 1280 720 let count = MapGen.visibleTileCount 256 192 1000 1000 1280 720 Assert.AreEqual((x1 - x0 + 1) * (y1 - y0 + 1), count) Assert.IsTrue(count < total, "viewport culling must draw strictly fewer tiles than the whole map") let ox0, oy0, ox1, oy1 = MapGen.visibleTileRange 256 192 0 0 1280 720 Assert.AreEqual(0, ox0) Assert.AreEqual(0, oy0) Assert.AreEqual(1280 / Sim.tilePixels + 1, ox1) Assert.AreEqual(720 / Sim.tilePixels + 1, oy1) // ---- P20 第二步:河道连续性 + 桥/路/建筑 + 交通可达 ---- [] member _.RiversAreContinuousWithoutGapsOrIsolatedTiles () = let map = MapGen.generateWithSize 256 192 (uint64 4242) Assert.IsTrue(map.Rivers.Length >= 1, "at least one river band") let minWidth = map.Rivers |> List.map (fun river -> river.Width) |> List.min Assert.IsTrue(minWidth >= 3, "river width lower bound") for x in 1 .. map.Width - 2 do let runs = waterRunsInColumn map x if runs.Length <> map.Rivers.Length then Assert.Fail(sprintf "column %d must hold exactly %d contiguous runs, saw %d" x map.Rivers.Length runs.Length) for run in runs do Assert.IsTrue(run >= minWidth, sprintf "column %d has a run of %d < min width %d" x run minWidth) for y in 0 .. map.Height - 1 do for x in 0 .. map.Width - 1 do if map.Tiles.[y * map.Width + x] = int MapGen.GroundTile.Water then Assert.IsTrue(waterNeighbourCount map x y >= 2, sprintf "water tile (%d,%d) is isolated" x y) [] member _.StructuresContainBridgesRoadsAndBuildingsAlongRivers () = let map = MapGen.generateWithSize 256 192 (uint64 4242) Assert.IsTrue(map.Bridges.Length >= map.Rivers.Length, "at least one bridge per river") Assert.IsTrue(map.Paths.Length > 0, "stone road network exists") Assert.IsTrue(map.Buildings.Length >= 4, "at least four riverside building blocks") for building in map.Buildings do Assert.IsTrue(MapGen.isWalkable map building.DoorX building.DoorY, sprintf "door (%d,%d) must be walkable" building.DoorX building.DoorY) let nearWater = [ for y in max 0 (building.Top - 12) .. min (map.Height - 1) (building.Top + building.Height + 12) do for x in max 0 (building.Left - 12) .. min (map.Width - 1) (building.Left + building.Width + 12) -> map.Tiles.[y * map.Width + x] = int MapGen.GroundTile.Water ] |> List.exists id Assert.IsTrue(nearWater, "building block must sit along a river") [] member _.BridgesKeepBothRiverBanksReachableFromSpawns () = let map = MapGen.generateWithSize 256 192 (uint64 4242) let startX, startY = map.Spawns.Head let visited = MapGen.floodFill map startX startY for (x, y) in map.Spawns do Assert.IsTrue(visited.[y * map.Width + x], sprintf "spawn (%d,%d) unreachable" x y) for building in map.Buildings do Assert.IsTrue(visited.[building.DoorY * map.Width + building.DoorX], "building door unreachable via bridge/road") for (x, y) in map.Bridges do Assert.IsTrue(visited.[y * map.Width + x], sprintf "bridge tile (%d,%d) unreachable" x y) Assert.IsTrue(map.BridgeCrossingsOk, "each bridge must connect both river banks") [] member _.StructuresAreDeterministicAcrossRuns () = let a = MapGen.generateWithSize 256 192 (uint64 4242) let b = MapGen.generateWithSize 256 192 (uint64 4242) Assert.IsTrue(a.Rivers = b.Rivers) Assert.IsTrue(a.Bridges = b.Bridges) Assert.IsTrue(a.Paths = b.Paths) Assert.IsTrue(a.Buildings = b.Buildings) let c = MapGen.generateWithSize 256 192 (uint64 7) Assert.IsTrue(a.Buildings <> c.Buildings || a.Bridges <> c.Bridges, "different seeds must relocate structures") // ---- P20 第三步:序列化确定性 + 默认可玩世界接入生成器站位 ---- [] member _.SameSeedSerializationIsByteIdentical () = let a = MapGen.generateWithSize 256 192 (uint64 4242) let b = MapGen.generateWithSize 256 192 (uint64 4242) Assert.AreEqual(MapGen.serialize a, MapGen.serialize b) let c = MapGen.generateWithSize 256 192 (uint64 7) Assert.AreNotEqual(MapGen.serialize a, MapGen.serialize c) [] member _.MapWorldPlacementUsesReachableCoreSpawns () = let map = MapGen.generateWithSize 256 192 (uint64 4242) let world = WorldBootstrap.initialWorldInMap false 42UL Sim.npcCount map Assert.AreEqual(Sim.npcCount, world.Npcs.Length) let visited = MapGen.floodFill map (fst map.Spawns.Head) (snd map.Spawns.Head) for npc in world.Npcs do let tx = int npc.Pos.X / Sim.tilePixels let ty = int npc.Pos.Y / Sim.tilePixels Assert.IsTrue(visited.[ty * map.Width + tx], sprintf "npc tile (%d,%d) must be reachable from the core" tx ty) let ax = int world.Avatar.Pos.X / Sim.tilePixels let ay = int world.Avatar.Pos.Y / Sim.tilePixels Assert.IsTrue(visited.[ay * map.Width + ax], "avatar spawn must sit in the reachable core") Assert.IsTrue(ax >= map.Core.MinX && ax <= map.Core.MaxX && ay >= map.Core.MinY && ay <= map.Core.MaxY, "avatar spawn must sit in the core") // ---- P23:512x384 扩程(尺寸自适应参数 + 连续性/可达性 + 默认图锁定) ---- [] member _.SizeAdaptiveParamsScaleRiversWithHeight () = Assert.AreEqual(2, (MapGen.paramsForSize 64 48 (uint64 1)).RiverCount) Assert.AreEqual(2, (MapGen.paramsForSize 256 192 (uint64 1)).RiverCount) Assert.AreEqual(4, (MapGen.paramsForSize 512 384 (uint64 1)).RiverCount) [] member _.HugeMap512SameSeedIsByteIdentical () = let a = MapGen.generateWithSize 512 384 (uint64 4242) let b = MapGen.generateWithSize 512 384 (uint64 4242) tilesEqual a b "512x384 same seed" Assert.AreEqual(MapGen.serialize a, MapGen.serialize b) Assert.AreEqual(512, a.Width) Assert.AreEqual(384, a.Height) Assert.AreEqual(30, a.Spawns.Length) Assert.IsTrue(a.ReachabilityOk, "512x384 must keep the core reachable") Assert.IsTrue(a.BridgeCrossingsOk, "512x384 bridges must connect both banks") [] member _.HugeMap512DifferentSeedsDiverge () = let a = MapGen.generateWithSize 512 384 (uint64 4242) let b = MapGen.generateWithSize 512 384 (uint64 7) Assert.AreNotEqual(checksum a, checksum b, "different seeds must differ at 512x384") Assert.AreNotEqual(MapGen.serialize a, MapGen.serialize b) [] member _.HugeMap512RiversStayContinuousAcrossAllBands () = let map = MapGen.generateWithSize 512 384 (uint64 4242) Assert.AreEqual(4, map.Rivers.Length) let minWidth = map.Rivers |> List.map (fun river -> river.Width) |> List.min Assert.IsTrue(minWidth >= 3, "river width lower bound holds at 512x384") for x in 1 .. map.Width - 2 do let runs = waterRunsInColumn map x if runs.Length <> map.Rivers.Length then Assert.Fail(sprintf "column %d must hold exactly %d contiguous runs, saw %d" x map.Rivers.Length runs.Length) for run in runs do Assert.IsTrue(run >= minWidth, sprintf "column %d has a run of %d < min width %d" x run minWidth) for y in 0 .. map.Height - 1 do for x in 0 .. map.Width - 1 do if map.Tiles.[y * map.Width + x] = int MapGen.GroundTile.Water then Assert.IsTrue(waterNeighbourCount map x y >= 2, sprintf "water tile (%d,%d) is isolated" x y) [] member _.HugeMap512ThirtySpawnsReachCoreAndBothBanks () = let map = MapGen.generateWithSize 512 384 (uint64 4242) let startX, startY = map.Spawns.Head let visited = MapGen.floodFill map startX startY for (x, y) in map.Spawns do Assert.IsTrue(visited.[y * map.Width + x], sprintf "spawn (%d,%d) unreachable at 512x384" x y) for building in map.Buildings do Assert.IsTrue(visited.[building.DoorY * map.Width + building.DoorX], "building door unreachable at 512x384") for (x, y) in map.Bridges do Assert.IsTrue(visited.[y * map.Width + x], sprintf "bridge tile (%d,%d) unreachable at 512x384" x y) Assert.IsTrue(map.Buildings.Length >= map.Rivers.Length, "riverside building per band at 512x384") // ---- P24:512x384 沿河民居加密(默认图不受影响) ---- [] member _.ExtraHouseDensityOnlyEnabledForLargeMaps () = Assert.AreEqual(0, (MapGen.paramsForSize 64 48 (uint64 1)).ExtraRiversideHouses) Assert.AreEqual(0, (MapGen.paramsForSize 256 192 (uint64 1)).ExtraRiversideHouses) Assert.IsTrue((MapGen.paramsForSize 512 384 (uint64 1)).ExtraRiversideHouses > 0) [] member _.LargeMapHasAtLeastTwelveHousesWithReachableDoors () = let map = MapGen.generateWithSize 512 384 (uint64 4242) Assert.IsTrue(map.Buildings.Length >= 12, sprintf "512x384 must place >=12 houses, saw %d" map.Buildings.Length) let startX, startY = map.Spawns.Head let visited = MapGen.floodFill map startX startY for building in map.Buildings do Assert.IsTrue(MapGen.isWalkable map building.DoorX building.DoorY, sprintf "door (%d,%d) must be walkable" building.DoorX building.DoorY) Assert.IsTrue(visited.[building.DoorY * map.Width + building.DoorX], sprintf "door (%d,%d) unreachable" building.DoorX building.DoorY) Assert.IsTrue(map.ReachabilityOk, "512x384 reachability must hold with extra houses") [] member _.LargeMapHousePlacementIsDeterministic () = let a = MapGen.generateWithSize 512 384 (uint64 4242) let b = MapGen.generateWithSize 512 384 (uint64 4242) Assert.AreEqual(a.Buildings.Length, b.Buildings.Length) Assert.IsTrue(a.Buildings = b.Buildings) Assert.AreEqual(MapGen.serialize a, MapGen.serialize b) /// P28/P30:512 沿河民居增密并出现 2/3/4 三档宽度(附属小筑/民居/大宅),形成聚落层次; /// 默认图仍为 0,不消耗 RNG,故 64/256 checksum 锁定不变。 [] member _.LargeMapHouseDensityHierarchyOnlyGrowsLargeMaps () = Assert.AreEqual(30, (MapGen.paramsForSize 512 384 (uint64 1)).ExtraRiversideHouses) Assert.AreEqual(0, (MapGen.paramsForSize 64 48 (uint64 1)).ExtraRiversideHouses) Assert.AreEqual(0, (MapGen.paramsForSize 256 192 (uint64 1)).ExtraRiversideHouses) let map = MapGen.generateWithSize 512 384 (uint64 4242) Assert.IsTrue(map.Buildings.Length >= 28, sprintf "P30 512 must place >=28 houses, saw %d" map.Buildings.Length) let widths = map.Buildings |> List.map (fun b -> b.Width) |> Set.ofList Assert.IsTrue(widths.Contains 2, "outbuilding width 2 must appear") Assert.IsTrue(widths.Contains 3, "house width 3 must appear") Assert.IsTrue(widths.Contains 4, "hall width 4 must appear") Assert.IsTrue(map.Buildings.Length + map.Farmhouses.Length >= 32, "total houses (clusters + farmsteads) must be >=32") [] member _.DefaultSizeHouseCountUnchanged () = Assert.AreEqual(4, (MapGen.generateWithSize 64 48 (uint64 4242)).Buildings.Length) Assert.AreEqual(4, (MapGen.generateWithSize 256 192 (uint64 4242)).Buildings.Length) // ---- P29:512 聚落组团(group seeding 仅大图启用;行内成组、组间留档) ---- [] member _.ClusterSeedOnlyEnabledForLargeMaps () = Assert.AreEqual(0UL, (MapGen.paramsForSize 64 48 (uint64 4242)).ClusterSeed) Assert.AreEqual(0UL, (MapGen.paramsForSize 256 192 (uint64 4242)).ClusterSeed) Assert.AreNotEqual(0UL, (MapGen.paramsForSize 512 384 (uint64 4242)).ClusterSeed) Assert.AreEqual(0UL, (MapGen.defaultParams (uint64 4242)).ClusterSeed) [] member _.LargeMapHousesFormClusteredRowsWithControlledGaps () = let map = MapGen.generateWithSize 512 384 (uint64 4242) let rowClusters (row: MapGen.Building list) = let sorted = row |> List.sortBy (fun b -> b.Left) let clusters = ResizeArray() let mutable current: MapGen.Building list = [] let mutable previousEnd = System.Int32.MinValue for building in sorted do if current <> [] && building.Left - previousEnd <= 2 then current <- building :: current else if current <> [] then clusters.Add(List.rev current) current <- [ building ] previousEnd <- building.Left + building.Width - 1 if current <> [] then clusters.Add(List.rev current) List.ofSeq clusters let clusteredRows = map.Buildings |> List.groupBy (fun b -> b.Top) |> List.map (snd >> rowClusters) let clusterSizes = clusteredRows |> List.collect (List.map List.length) Assert.IsTrue(clusterSizes |> List.exists (fun n -> n >= 3), "at least one row-sized house cluster") // 行内出现多个簇 => 组间留有可控空档,而不是一整排等距。 let rowCount = map.Buildings |> List.map (fun b -> b.Top) |> List.distinct |> List.length Assert.IsTrue(clusterSizes.Length > rowCount, "rows must split into separated groups") for clustersInRow in clusteredRows do match clustersInRow with | first :: rest -> let mutable previous = first for cluster in rest do let prevEnd = previous |> List.map (fun b -> b.Left + b.Width - 1) |> List.max let nextStart = cluster |> List.map (fun b -> b.Left) |> List.min Assert.IsTrue(nextStart - prevEnd >= 3, "groups must be separated by a controlled gap") previous <- cluster | [] -> () /// 组团参数下,512 world 同 seed 两次生成逐字节一致(checksum 且序列化 diff 为空)。 [] member _.LargeMapClusterLayoutChecksumIsEmptyDiffAcrossRuns () = let a = MapGen.generateWithSize 512 384 (uint64 4242) let b = MapGen.generateWithSize 512 384 (uint64 4242) Assert.IsTrue(a.Tiles = b.Tiles, "tiles must be byte-identical across runs") Assert.AreEqual(checksum a, checksum b) Assert.AreEqual(MapGen.serialize a, MapGen.serialize b) Assert.IsTrue(a.Buildings = b.Buildings) Assert.IsTrue(a.Decorations = b.Decorations) /// P29 垂柳成丛:岸边出现相邻 2-3 株的柳丛,且不再是均匀逐格散布。 [] member _.LargeMapWillowsGrowInGroves () = let map = MapGen.generateWithSize 512 384 (uint64 4242) let willows = map.Decorations |> List.filter (fun d -> d.Kind = MapGen.Willow) Assert.IsTrue(willows.Length > 0, "large map must place willows") let groveSizes = willows |> List.groupBy (fun d -> d.Y) |> List.collect (fun (_, row) -> let xs = row |> List.map (fun d -> d.X) |> List.sort let sizes = ResizeArray() let mutable current = 0 let mutable previous = System.Int32.MinValue for x in xs do if previous >= 0 && x - previous = 1 then current <- current + 1 else if current > 0 then sizes.Add current current <- 1 previous <- x if current > 0 then sizes.Add current List.ofSeq sizes) Assert.IsTrue(groveSizes |> List.exists (fun n -> n >= 2), "willows must form groves of adjacent trunks") // ---- P25:大图不产孤立石板 tile(装饰石块默认开启仅限小图) ---- [] member _.DecorativeStonesDisabledOnLargeMaps () = Assert.IsTrue((MapGen.paramsForSize 64 48 (uint64 1)).DecorativeStones) Assert.IsTrue((MapGen.paramsForSize 256 192 (uint64 1)).DecorativeStones) Assert.IsFalse((MapGen.paramsForSize 512 384 (uint64 1)).DecorativeStones) [] member _.LargeMapHasNoIsolatedStoneTiles () = let map = MapGen.generateWithSize 512 384 (uint64 4242) let pathSet = System.Collections.Generic.HashSet(map.Paths |> List.map (fun (x, y) -> y * map.Width + x)) let stone x y = map.Tiles.[y * map.Width + x] = int MapGen.GroundTile.Stone let mutable isolated = 0 for y in 0 .. map.Height - 1 do for x in 0 .. map.Width - 1 do if stone x y && not (pathSet.Contains(y * map.Width + x)) then let neighbours = [ (x + 1, y); (x - 1, y); (x, y + 1); (x, y - 1) ] |> List.filter (fun (nx, ny) -> nx >= 0 && nx < map.Width && ny >= 0 && ny < map.Height) |> List.filter (fun (nx, ny) -> stone nx ny) |> List.length if neighbours = 0 then isolated <- isolated + 1 Assert.AreEqual(0, isolated) // 装饰关闭后,大图所有石板都应属于路网。 let stonesOutsidePaths = [ for y in 0 .. map.Height - 1 do for x in 0 .. map.Width - 1 do if stone x y && not (pathSet.Contains(y * map.Width + x)) then yield (x, y) ] Assert.AreEqual(0, stonesOutsidePaths.Length) /// 锁定当前默认尺寸的生成结果,防止 P23 参数适配无意改变 64x48 / 256x192。 [] member _.DefaultSizeChecksumsArePinned () = Assert.AreEqual(6042052L, checksum (MapGen.generateWithSize 64 48 (uint64 4242))) Assert.AreEqual(1284656962L, checksum (MapGen.generateWithSize 256 192 (uint64 4242))) // ---- P26:河岸装饰(护岸石垒/芦苇/垂柳)仅大图、落在岸格且不挡通行 ---- [] member _.RiverDecorationsOnlyEnabledOnLargeMaps () = Assert.IsFalse((MapGen.paramsForSize 64 48 (uint64 1)).RiverDecorations) Assert.IsFalse((MapGen.paramsForSize 256 192 (uint64 1)).RiverDecorations) Assert.IsTrue((MapGen.paramsForSize 512 384 (uint64 1)).RiverDecorations) Assert.AreEqual(0, (MapGen.generateWithSize 64 48 (uint64 4242)).Decorations.Length) Assert.AreEqual(0, (MapGen.generateWithSize 256 192 (uint64 4242)).Decorations.Length) [] member _.RiverbankDecorationsSitOnBanksAndNeverBlockPathsOrBridges () = let map = MapGen.generateWithSize 512 384 (uint64 4242) Assert.IsTrue(map.Decorations.Length > 0, "512x384 must place riverbank decorations") let pathSet = System.Collections.Generic.HashSet(map.Paths |> List.map (fun (x, y) -> y * map.Width + x)) let bridgeSet = System.Collections.Generic.HashSet(map.Bridges |> List.map (fun (x, y) -> y * map.Width + x)) let water x y = map.Tiles.[y * map.Width + x] = int MapGen.GroundTile.Water let inCore x y = x >= map.Core.MinX && x <= map.Core.MaxX && y >= map.Core.MinY && y <= map.Core.MaxY for decoration in map.Decorations do let footprint = match decoration.Kind with | MapGen.Willow -> [ decoration.Y; decoration.Y + 1 ] | _ -> [ decoration.Y ] for y in footprint do let i = y * map.Width + decoration.X Assert.AreNotEqual(int MapGen.GroundTile.Water, map.Tiles.[i], "decoration must sit on land") Assert.IsFalse(pathSet.Contains i, "decoration must not block a path tile") Assert.IsFalse(bridgeSet.Contains i, "decoration must not block a bridge tile") Assert.IsFalse(inCore decoration.X y, "decoration must stay outside the core") // Bank adjacency: at least one footprint tile must touch the river orthogonally. let touchesWater = footprint |> List.exists (fun y -> [ (decoration.X + 1, y); (decoration.X - 1, y); (decoration.X, y + 1); (decoration.X, y - 1) ] |> List.exists (fun (nx, ny) -> nx >= 0 && nx < map.Width && ny >= 0 && ny < map.Height && water nx ny)) Assert.IsTrue(touchesWater, "decoration must line the riverbank, not float inland") [] member _.LargeMapRiverDecorationsAreDeterministic () = let a = MapGen.generateWithSize 512 384 (uint64 4242) let b = MapGen.generateWithSize 512 384 (uint64 4242) Assert.IsTrue(a.Decorations = b.Decorations) Assert.AreEqual(MapGen.serialize a, MapGen.serialize b) let c = MapGen.generateWithSize 512 384 (uint64 7) Assert.AreNotEqual(MapGen.serialize a, MapGen.serialize c) // ---- P30:512 民居增密 + 组团规模 + 乡村空区填充 + 主路柔化 ---- [] member _.LargeMapGroupsAreTwoToFiveWithFewIsolated () = let map = MapGen.generateWithSize 512 384 (uint64 4242) let groups = houseGroups map.Buildings Assert.IsTrue(groups.Length > 0, "512 must form house groups") let singletons = groups |> List.filter (fun n -> n < 2) |> List.length Assert.IsTrue(singletons <= 2, sprintf "isolated single-house groups must be <=2, saw %d" singletons) let inRange = groups |> List.filter (fun n -> n >= 2 && n <= 5) |> List.length Assert.IsTrue(inRange * 2 >= groups.Length, "most groups must hold 2-5 houses") // 另一 seed 同样不产孤立组,验证不是对 4242 的过拟合。 let other = houseGroups (MapGen.generateWithSize 512 384 (uint64 7)).Buildings Assert.IsTrue((other |> List.filter (fun n -> n < 2) |> List.length) <= 2) [] member _.LargeMapCountrysideFillsEmptyBandsWithMatureTiles () = let map = MapGen.generateWithSize 512 384 (uint64 4242) let isField code = code = int MapGen.GroundTile.PaddyField || code = int MapGen.GroundTile.VegetablePlot let paddy = map.Tiles |> Array.filter (fun c -> c = int MapGen.GroundTile.PaddyField) |> Array.length let veg = map.Tiles |> Array.filter (fun c -> c = int MapGen.GroundTile.VegetablePlot) |> Array.length Assert.IsTrue(paddy > 0, "paddy fields must appear on 512") Assert.IsTrue(veg > 0, "vegetable plots must appear on 512") Assert.IsTrue(paddy + veg > 4000, sprintf "fields must cover a meaningful share, saw %d" (paddy + veg)) Assert.IsTrue(map.Groves.Length > 0, "tree groves must appear") Assert.IsTrue(map.Farmhouses.Length > 0, "scattered farmsteads must appear") // 上部/下部空带都要有田块,避免大面积纯草。 let topBand = [ for y in 0 .. 90 do for x in 0 .. map.Width - 1 -> map.Tiles.[y * map.Width + x] ] |> List.filter isField |> List.length let bottomBand = [ for y in 296 .. map.Height - 1 do for x in 0 .. map.Width - 1 -> map.Tiles.[y * map.Width + x] ] |> List.filter isField |> List.length Assert.IsTrue(topBand > 100, sprintf "top band must contain fields, saw %d" topBand) Assert.IsTrue(bottomBand > 100, sprintf "bottom band must contain fields, saw %d" bottomBand) // 默认图完全不进入乡村分支。 let small = MapGen.generateWithSize 256 192 (uint64 4242) Assert.AreEqual(0, small.Groves.Length) Assert.AreEqual(0, small.Farmhouses.Length) Assert.AreEqual(0, small.Tiles |> Array.filter isField |> Array.length) [] member _.FieldsStayOnLandAwayFromWaterPathsAndCore () = let map = MapGen.generateWithSize 512 384 (uint64 4242) let pathSet = System.Collections.Generic.HashSet(map.Paths |> List.map (fun (x, y) -> y * map.Width + x)) let isField code = code = int MapGen.GroundTile.PaddyField || code = int MapGen.GroundTile.VegetablePlot for y in 0 .. map.Height - 1 do for x in 0 .. map.Width - 1 do let i = y * map.Width + x if isField map.Tiles.[i] then Assert.AreNotEqual(int MapGen.GroundTile.Water, map.Tiles.[i]) Assert.IsFalse(pathSet.Contains i, sprintf "field (%d,%d) must not sit on a path" x y) let inCore = x >= map.Core.MinX && x <= map.Core.MaxX && y >= map.Core.MinY && y <= map.Core.MaxY Assert.IsFalse(inCore, sprintf "field (%d,%d) must stay outside the core" x y) [] member _.LargeMapFarmsteadDoorsAreReachableAndDeterministic () = let map = MapGen.generateWithSize 512 384 (uint64 4242) Assert.IsTrue(map.ReachabilityOk, "512 reachability must hold with farmsteads") let visited = MapGen.floodFill map (fst map.Spawns.Head) (snd map.Spawns.Head) for farmhouse in map.Farmhouses do Assert.IsTrue(MapGen.isWalkable map farmhouse.DoorX farmhouse.DoorY, sprintf "farmstead door (%d,%d) must be walkable" farmhouse.DoorX farmhouse.DoorY) Assert.IsTrue(visited.[farmhouse.DoorY * map.Width + farmhouse.DoorX], sprintf "farmstead door (%d,%d) unreachable" farmhouse.DoorX farmhouse.DoorY) let other = MapGen.generateWithSize 512 384 (uint64 4242) Assert.IsTrue(map.Farmhouses = other.Farmhouses) Assert.IsTrue(map.Groves = other.Groves) Assert.IsTrue(map.Tiles = other.Tiles) Assert.AreEqual(MapGen.serialize map, MapGen.serialize other) [] member _.LargeMapMainRoadIsDeterministicSerpentine () = let map = MapGen.generateWithSize 512 384 (uint64 4242) let other = MapGen.generateWithSize 512 384 (uint64 4242) // 同 seed 两次逐字节一致(含主路中心线)。 Assert.IsTrue(map.MainRoadCenter = other.MainRoadCenter, "road centerline must be byte-identical across runs") Assert.AreEqual(MapGen.serialize map, MapGen.serialize other) // 逐点微扰:相邻列步长 <=1,且相对中段基准确有 >=2 格弯曲。 map.MainRoadCenter |> Array.pairwise |> Array.iter (fun (a, b) -> Assert.IsTrue(abs (b - a) <= 1, sprintf "road step must be <=1, saw %d -> %d" a b)) let baseY = map.MainRoadCenter.[map.Width / 2] Assert.IsTrue(map.MainRoadCenter |> Array.exists (fun y -> abs (y - baseY) >= 2), "main road must bend by >=2 tiles") // 默认图主街仍笔直(不进入新分支)。 let small = MapGen.generateWithSize 256 192 (uint64 4242) let smallBase = small.MainRoadCenter.[small.Width / 2] Assert.IsTrue(small.MainRoadCenter |> Array.forall (fun y -> y = smallBase), "default map road must stay straight") [] member _.VisibleRangeClampsAtLegacyMapEdges () = let x0, y0, x1, y1 = MapGen.visibleTileRange 64 48 0 0 2048 1536 Assert.AreEqual(0, x0) Assert.AreEqual(0, y0) Assert.AreEqual(63, x1) Assert.AreEqual(47, y1) Assert.AreEqual(64 * 48, MapGen.visibleTileCount 64 48 0 0 2048 1536)