summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop
diff options
context:
space:
mode:
Diffstat (limited to 'src/LivingVillage.Desktop')
-rw-r--r--src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj1
-rw-r--r--src/LivingVillage.Desktop/MapSnapshot.fs125
-rw-r--r--src/LivingVillage.Desktop/Program.fs48
3 files changed, 171 insertions, 3 deletions
diff --git a/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj b/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj
index ab0a906..7243705 100644
--- a/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj
+++ b/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj
@@ -9,6 +9,7 @@
<Compile Include="VillagePresentation.fs" />
<Compile Include="ProceduralMap.fs" />
<Compile Include="MapGen.fs" />
+ <Compile Include="MapSnapshot.fs" />
<Compile Include="WorldBootstrap.fs" />
<Compile Include="TaskRuntime.fs" />
<Compile Include="PlayerTrade.fs" />
diff --git a/src/LivingVillage.Desktop/MapSnapshot.fs b/src/LivingVillage.Desktop/MapSnapshot.fs
new file mode 100644
index 0000000..498ef48
--- /dev/null
+++ b/src/LivingVillage.Desktop/MapSnapshot.fs
@@ -0,0 +1,125 @@
+namespace LivingVillage.Desktop
+
+open System
+open System.IO
+open System.IO.Compression
+open System.Text
+
+/// P68:把 `MapGen.Result` 离线渲染为整图 PNG(不依赖图形设备,纯 CPU)。
+///
+/// 用途:为「512×384 整图」方案验收补一张覆盖全图的真实像素证据。
+/// 判据口径由 `scripts/analyze-p68.py` 复用(调色板常量与此处一一对应):
+/// * 每瓦片 `scale`(默认 8)像素,输出尺寸 = 宽×scale × 高×scale;
+/// * 河流(Water 瓦片)、主路(MainRoadCenter)、农田(PaddyField/VegetablePlot)、
+/// 民居(Buildings/Farmhouses 屋顶矩形)均有独立可辨色;
+/// * 纯函数:同输入逐字节一致;无外部素材依赖。
+module MapSnapshot =
+
+ /// 默认每瓦片像素(>= 8,满足证据可辨要求)。
+ let defaultScale = 8
+
+ let private grass = (86uy, 140uy, 74uy)
+ let private water = (52uy, 110uy, 180uy)
+ let private stone = (150uy, 150uy, 150uy)
+ let private peat = (110uy, 96uy, 72uy)
+ let private paddy = (120uy, 168uy, 84uy)
+ let private vegetable = (154uy, 186uy, 96uy)
+ let private path = (196uy, 176uy, 120uy)
+ let private road = (214uy, 190uy, 128uy)
+ let private bridge = (140uy, 96uy, 64uy)
+ let private roof = (58uy, 58uy, 70uy)
+ let private door = (120uy, 72uy, 40uy)
+ let private grove = (34uy, 96uy, 52uy)
+
+ let private tileColor (code: int) : byte * byte * byte =
+ match code with
+ | 1 -> water
+ | 2 -> stone
+ | 3 -> peat
+ | 4 -> paddy
+ | 5 -> vegetable
+ | _ -> grass
+
+ /// 把 `MapGen.Result` 渲染为 RGB 像素缓冲(行优先,左上原点),每瓦片 `scale` 像素。
+ let renderRgb (map: MapGen.Result) (scale: int) : byte array =
+ let s = max 1 scale
+ let width = map.Width * s
+ let height = map.Height * s
+ let pixels = Array.zeroCreate<byte> (width * height * 3)
+ let fillTile (tx: int) (ty: int) (r: byte, g: byte, b: byte) =
+ if tx >= 0 && tx < map.Width && ty >= 0 && ty < map.Height then
+ for py in ty * s .. ty * s + s - 1 do
+ for px in tx * s .. tx * s + s - 1 do
+ let i = (py * width + px) * 3
+ pixels.[i] <- r
+ pixels.[i + 1] <- g
+ pixels.[i + 2] <- b
+ let fillRect (left: int) (top: int) (rectWidth: int) (rectHeight: int) (r: byte, g: byte, b: byte) =
+ for ty in top .. top + max 0 rectHeight - 1 do
+ for tx in left .. left + max 0 rectWidth - 1 do
+ fillTile tx ty (r, g, b)
+ for ty in 0 .. map.Height - 1 do
+ for tx in 0 .. map.Width - 1 do
+ fillTile tx ty (tileColor map.Tiles.[ty * map.Width + tx])
+ for (x, y) in map.Paths do
+ fillTile x y path
+ map.MainRoadCenter
+ |> Array.iteri (fun x y -> fillTile x y road)
+ for (x, y) in map.Bridges do
+ fillTile x y bridge
+ let drawBuilding (building: MapGen.Building) =
+ fillRect building.Left building.Top building.Width building.Height roof
+ fillTile building.DoorX building.DoorY door
+ for building in map.Buildings do
+ drawBuilding building
+ for farmhouse in map.Farmhouses do
+ drawBuilding farmhouse
+ for g in map.Groves do
+ fillTile g.X g.Y grove
+ pixels
+
+ let private crcTable =
+ [| for n in 0 .. 255 ->
+ let mutable c = uint32 n
+ for _ in 0 .. 7 do
+ c <- if (c &&& 1u) = 1u then 0xEDB88320u ^^^ (c >>> 1) else c >>> 1
+ c |]
+
+ let private crc32 (bytes: byte array) : uint32 =
+ let mutable c = 0xFFFFFFFFu
+ for b in bytes do
+ c <- crcTable.[int ((c ^^^ uint32 b) &&& 0xFFu)] ^^^ (c >>> 8)
+ c ^^^ 0xFFFFFFFFu
+
+ let private be32 (value: uint32) : byte array =
+ [| byte (value >>> 24); byte ((value >>> 16) &&& 0xFFu); byte ((value >>> 8) &&& 0xFFu); byte (value &&& 0xFFu) |]
+
+ let private chunk (kind: string) (data: byte array) : byte array =
+ let kindBytes = Encoding.ASCII.GetBytes kind
+ let crc = crc32 (Array.append kindBytes data)
+ Array.concat [ be32 (uint32 data.Length); kindBytes; data; be32 crc ]
+
+ /// zlib 容器压缩(BCL `ZLibStream`,无外部依赖),PNG IDAT 直接用。
+ let private zlibCompress (data: byte array) : byte array =
+ use output = new MemoryStream()
+ using (new ZLibStream(output, CompressionLevel.Optimal, true)) (fun zlib ->
+ zlib.Write(data, 0, data.Length))
+ output.ToArray()
+
+ /// 渲染为 8-bit RGB PNG 字节流(无 alpha)。确定性:同输入逐字节一致。
+ let renderPng (map: MapGen.Result) (scale: int) : byte array =
+ let s = max 1 scale
+ let width = map.Width * s
+ let height = map.Height * s
+ let rgb = renderRgb map s
+ let stride = width * 3
+ let raw = Array.zeroCreate<byte> (height * (1 + stride))
+ for y in 0 .. height - 1 do
+ raw.[y * (1 + stride)] <- 0uy
+ Array.blit rgb (y * stride) raw (y * (1 + stride) + 1) stride
+ let signature = [| 137uy; 80uy; 78uy; 71uy; 13uy; 10uy; 26uy; 10uy |]
+ let ihdr = Array.concat [ be32 (uint32 width); be32 (uint32 height); [| 8uy; 2uy; 0uy; 0uy; 0uy |] ]
+ Array.concat [ signature; chunk "IHDR" ihdr; chunk "IDAT" (zlibCompress raw); chunk "IEND" [||] ]
+
+ let save (path: string) (map: MapGen.Result) (scale: int) : unit =
+ File.WriteAllBytes(path, renderPng map scale)
diff --git a/src/LivingVillage.Desktop/Program.fs b/src/LivingVillage.Desktop/Program.fs
index 0e2adcf..53cc274 100644
--- a/src/LivingVillage.Desktop/Program.fs
+++ b/src/LivingVillage.Desktop/Program.fs
@@ -1,9 +1,51 @@
namespace LivingVillage.Desktop
+open System
+open System.IO
+open System.Security.Cryptography
+
module Program =
+ /// P68 证据钩子:LV_P68_MAPSHOT=1 时离线渲染 512×384 / seed 4242 整图为 PNG 后退出,
+ /// 不启动图形设备(真正 headless)。打印尺寸/瓦片统计/要素统计/确定性供 analyze 核验。
+ let private runMapShot () : int =
+ let map = MapGen.generateWithSize 512 384 (uint64 4242)
+ let scale = MapSnapshot.defaultScale
+ let recordDirectory =
+ match Environment.GetEnvironmentVariable("LV_RECORD_DIR") with
+ | null | "" -> "/tmp/lv-p68-map"
+ | value -> value
+ Directory.CreateDirectory recordDirectory |> ignore
+ let png = MapSnapshot.renderPng map scale
+ let again = MapSnapshot.renderPng (MapGen.generateWithSize 512 384 (uint64 4242)) scale
+ let deterministic = png = again
+ let sha = SHA256.HashData png |> Convert.ToHexString
+ File.WriteAllBytes(Path.Combine(recordDirectory, "p68-map.png"), png)
+ let tiles code = map.Tiles |> Array.filter (fun c -> c = code) |> Array.length
+ let roadTiles =
+ map.MainRoadCenter
+ |> Array.filter (fun y -> y >= 0 && y < map.Height)
+ |> Array.length
+ printfn
+ "p68-map width=%d height=%d scale=%d png=%dx%d bytes=%d"
+ map.Width map.Height scale (map.Width * scale) (map.Height * scale) png.Length
+ printfn
+ "p68-tiles grass=%d water=%d stone=%d peat=%d paddy=%d vegetable=%d"
+ (tiles (int MapGen.GroundTile.Grass)) (tiles (int MapGen.GroundTile.Water))
+ (tiles (int MapGen.GroundTile.Stone)) (tiles (int MapGen.GroundTile.Peat))
+ (tiles (int MapGen.GroundTile.PaddyField)) (tiles (int MapGen.GroundTile.VegetablePlot))
+ printfn
+ "p68-features rivers=%d bridges=%d paths=%d buildings=%d farmhouses=%d groves=%d road_columns=%d"
+ map.Rivers.Length map.Bridges.Length map.Paths.Length map.Buildings.Length
+ map.Farmhouses.Length map.Groves.Length roadTiles
+ printfn "p68-render deterministic=%b sha256=%s" deterministic sha
+ 0
+
[<EntryPoint>]
let main _ =
- use game = new LivingVillageGame()
- game.Run()
- 0
+ if Environment.GetEnvironmentVariable("LV_P68_MAPSHOT") = "1" then
+ runMapShot ()
+ else
+ use game = new LivingVillageGame()
+ game.Run()
+ 0