diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-29 09:18:45 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-29 09:18:45 +0800 |
| commit | 4aa6fabbb594af1889778ff7f01ae3193dc08b1d (patch) | |
| tree | f25dea86c382d94bb005239001ef99da25fded37 /src/LivingVillage.Desktop/MapSnapshot.fs | |
| parent | a578791b0dd8ca95fba3af6f3c2f39c816bd7e52 (diff) | |
| download | living-village-4aa6fabbb594af1889778ff7f01ae3193dc08b1d.tar.gz | |
p68: 512×384 整图离线渲染截图证据
- MapSnapshot:MapGen.Result 纯 CPU 渲染为整图 PNG(每瓦片 8px,自包含
IHDR/IDAT/IEND + CRC32 + ZLibStream,无外部图像依赖);调色板覆盖
河流/主路/农田/民居屋顶/小径/桥/树丛
- Program.fs:LV_P68_MAPSHOT=1 钩子用 paramsForSize 512 384 seed=4242 生成并
离线渲染整图后退出(headless,不需图形设备)
- P68MapSnapshotTests(3 例,红先):尺寸/PNG 签名、要素像素存在、两次渲染逐字节一致
- scripts/analyze-p68.py:尺寸/调色板像素/无黑白大块/missing_glyph_slots=0/确定性
- Desktop 336/336、Kernel 119/119;digest 953775FA…7F6C ×3 PASS;未动 Kernel/Sim
Diffstat (limited to 'src/LivingVillage.Desktop/MapSnapshot.fs')
| -rw-r--r-- | src/LivingVillage.Desktop/MapSnapshot.fs | 125 |
1 files changed, 125 insertions, 0 deletions
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) |
