├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── .gitmodules ├── LICENCE ├── README.md ├── assets ├── Attribution.txt ├── Terrain.tsx ├── base-tiles.png ├── base-tiles.xcf └── terrain.png ├── build.zig ├── source-tiles.dat └── src └── main.zig /.gitattributes: -------------------------------------------------------------------------------- 1 | *.zig text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: MasterQ32 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache/ 2 | zig-out/ 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/zero-graphics"] 2 | path = vendor/zero-graphics 3 | url = https://github.com/MasterQ32/zero-graphics 4 | [submodule "vendor/s2s"] 5 | path = vendor/s2s 6 | url = https://github.com/ziglibs/s2s 7 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Felix "xq" Queißner 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 19 | OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wave-function-collapse-editor 2 | -------------------------------------------------------------------------------- /assets/Attribution.txt: -------------------------------------------------------------------------------- 1 | License 2 | ------- 3 | 4 | CC-BY-SA 3.0: 5 | - http://creativecommons.org/licenses/by-sa/3.0/ 6 | - See the file: cc-by-sa-3.0.txt 7 | GNU GPL 3.0: 8 | - http://www.gnu.org/licenses/gpl-3.0.html 9 | - See the file: gpl-3.0.txt 10 | 11 | Note the file is based on the LCP contest readme so don't expect the exact little pieces used like the base one. 12 | *Additional license information. 13 | 14 | Assets from: 15 | 16 | LPC contributors: 17 | ---------------- 18 | 19 | 20 | Matthew Nash 21 | http://opengameart.org/content/misc-32x32-tiles 22 | - Road 23 | 24 | Nushio 25 | http://opengameart.org/content/lpc-colorful-sand-deep-water 26 | - Snow and ice recolor 27 | 28 | William.Thompsonj 29 | http://opengameart.org/content/lpc-sandrock-alt-colors 30 | - Sand and dirt recolor 31 | 32 | Adrix89 33 | 34 | - Recolor Nushio snow 35 | 36 | Casper Nilsson 37 | *GNU GPL 3.0 or later 38 | email: casper.nilsson@gmail.com 39 | Freenode: CasperN 40 | OpenGameArt.org: C.Nilsson 41 | 42 | - LPC C.Nilsson (2D art) 43 | 44 | Daniel Eddeland 45 | *GNU GPL 3.0 or later 46 | - Tilesets of plants, props, food and environments, suitable for farming / fishing sims and other games. 47 | - Includes wheat, grass, sand tilesets, fence tilesets and plants such as corn and tomato. 48 | 49 | 50 | Johann CHARLOT 51 | *GNU LGPL Version 3. 52 | *Later versions are permitted. 53 | Homepage http://poufpoufproduction.fr 54 | Email johannc@poufpoufproduction.fr 55 | 56 | - Shoot'em up graphic kit 57 | 58 | Skyler Robert Colladay 59 | 60 | - FeralFantom's Entry (2D art) 61 | 62 | BASE assets: 63 | ------------ 64 | 65 | Lanea Zimmerman (AKA Sharm) 66 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 67 | 68 | - barrel.png 69 | - brackish.png 70 | - buckets.png 71 | - bridges.png 72 | - cabinets.png 73 | - cement.png 74 | - cementstair.png 75 | - chests.png 76 | - country.png 77 | - cup.png 78 | - dirt2.png 79 | - dirt.png 80 | - dungeon.png 81 | - grassalt.png 82 | - grass.png 83 | - holek.png 84 | - holemid.png 85 | - hole.png 86 | - house.png 87 | - inside.png 88 | - kitchen.png 89 | - lava.png 90 | - lavarock.png 91 | - mountains.png 92 | - rock.png 93 | - shadow.png 94 | - signs.png 95 | - stairs.png 96 | - treetop.png 97 | - trunk.png 98 | - waterfall.png 99 | - watergrass.png 100 | - water.png 101 | - princess.png and princess.xcf 102 | 103 | 104 | Stephen Challener (AKA Redshrike) 105 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 106 | 107 | - female_walkcycle.png 108 | - female_hurt.png 109 | - female_slash.png 110 | - female_spellcast.png 111 | - male_walkcycle.png 112 | - male_hurt.png 113 | - male_slash.png 114 | - male_spellcast.png 115 | - male_pants.png 116 | - male_hurt_pants.png 117 | - male_fall_down_pants.png 118 | - male_slash_pants.png 119 | 120 | 121 | Charles Sanchez (AKA CharlesGabriel) 122 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 123 | 124 | - bat.png 125 | - bee.png 126 | - big_worm.png 127 | - eyeball.png 128 | - ghost.png 129 | - man_eater_flower.png 130 | - pumpking.png 131 | - slime.png 132 | - small_worm.png 133 | - snake.png 134 | 135 | 136 | Manuel Riecke (AKA MrBeast) 137 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 138 | 139 | - hairfemale.png and hairfemale.xcf 140 | - hairmale.png and hairmale.xcf 141 | - soldier.png 142 | - soldier_altcolor.png 143 | 144 | 145 | Daniel Armstrong (AKA HughSpectrum) 146 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 147 | 148 | Castle work: 149 | 150 | - castlewalls.png 151 | - castlefloors.png 152 | - castle_outside.png 153 | - castlefloors_outside.png 154 | - castle_lightsources.png 155 | 156 | 157 | -------------------------------------------------------------------------------- /assets/Terrain.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | -------------------------------------------------------------------------------- /assets/base-tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikskuh/WaveFunctionCollapse/361f39a631fd17c6e6ca0fdd754bb62f19d4d5c6/assets/base-tiles.png -------------------------------------------------------------------------------- /assets/base-tiles.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikskuh/WaveFunctionCollapse/361f39a631fd17c6e6ca0fdd754bb62f19d4d5c6/assets/base-tiles.xcf -------------------------------------------------------------------------------- /assets/terrain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikskuh/WaveFunctionCollapse/361f39a631fd17c6e6ca0fdd754bb62f19d4d5c6/assets/terrain.png -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const ZeroSdk = @import("vendor/zero-graphics/Sdk.zig"); 4 | 5 | const pkgs = struct { 6 | const s2s = std.build.Pkg{ 7 | .name = "s2s", 8 | .path = .{ .path = "vendor/s2s/s2s.zig" }, 9 | }; 10 | }; 11 | 12 | pub fn build(b: *std.build.Builder) void { 13 | const target = b.standardTargetOptions(.{}); 14 | const mode = b.standardReleaseOptions(); 15 | 16 | const sdk = ZeroSdk.init(b, false); 17 | 18 | const app = sdk.createApplication("wfc_edit", "src/main.zig"); 19 | app.setBuildMode(mode); 20 | app.addPackage(pkgs.s2s); 21 | 22 | const desktop_app = app.compileFor(.{ .desktop = target }); 23 | 24 | desktop_app.install(); 25 | 26 | const run_desktop = desktop_app.run(); 27 | 28 | const run_step = b.step("run", "Run the app"); 29 | run_step.dependOn(&run_desktop.step); 30 | } 31 | -------------------------------------------------------------------------------- /source-tiles.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikskuh/WaveFunctionCollapse/361f39a631fd17c6e6ca0fdd754bb62f19d4d5c6/source-tiles.dat -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zero_graphics = @import("zero-graphics"); 3 | const s2s = @import("s2s"); 4 | 5 | const Application = @This(); 6 | 7 | const Rectangle = zero_graphics.Rectangle; 8 | const Color = zero_graphics.Color; 9 | 10 | allocator: std.mem.Allocator, 11 | input_queue: *zero_graphics.Input, 12 | 13 | rm: zero_graphics.ResourceManager, 14 | r2: zero_graphics.Renderer2D, 15 | 16 | screen_size: zero_graphics.Size, 17 | mouse_pos: zero_graphics.Point = .{ .x = 0, .y = 0 }, 18 | 19 | tile_set_tex: *zero_graphics.ResourceManager.Texture, 20 | 21 | tile_map: TileMap(?u8) = .{}, 22 | wave_function: TileMap(WaveFunction) = .{ 23 | .tiles = undefined, 24 | }, 25 | selected_draw_tile: u8 = 0, 26 | tile_info: [256]TileInfo, 27 | available_tiles: TileSet = TileSet.initEmpty(), 28 | 29 | random: std.rand.DefaultPrng, 30 | mode: Mode = .tile_editor, 31 | 32 | pub fn init(app: *Application, allocator: std.mem.Allocator, input_queue: *zero_graphics.Input) !void { 33 | app.* = .{ 34 | .allocator = allocator, 35 | .input_queue = input_queue, 36 | 37 | .r2 = undefined, 38 | .rm = undefined, 39 | .tile_set_tex = undefined, 40 | .tile_info = undefined, 41 | 42 | .screen_size = zero_graphics.Size.empty, 43 | .random = std.rand.DefaultPrng.init(@bitCast(u64, zero_graphics.milliTimestamp())), 44 | }; 45 | 46 | app.rm = zero_graphics.ResourceManager.init(allocator); 47 | errdefer app.rm.deinit(); 48 | 49 | app.r2 = try zero_graphics.Renderer2D.init(&app.rm, app.allocator); 50 | errdefer app.r2.deinit(); 51 | 52 | app.tile_set_tex = try app.rm.createTexture(.ui, zero_graphics.ResourceManager.DecodePng{ 53 | .data = @embedFile("../assets/base-tiles.png"), 54 | }); 55 | 56 | app.tile_info = if (std.fs.cwd().openFile("source-tiles.dat", .{})) |*file| blk: { 57 | defer file.close(); 58 | 59 | const neighbour_info_data: TileMap(?u8).Data = try s2s.deserialize(file.reader(), TileMap(?u8).Data); 60 | 61 | app.tile_map.tiles = neighbour_info_data; 62 | 63 | var tile_info = std.mem.zeroes([256]TileInfo); 64 | 65 | { 66 | var y: usize = 0; 67 | while (y < TileMapProto.height) : (y += 1) { 68 | var x: usize = 0; 69 | while (x < TileMapProto.width) : (x += 1) { 70 | const inspected_index = neighbour_info_data[y][x] orelse continue; 71 | const inspected = &tile_info[inspected_index]; 72 | 73 | app.available_tiles.set(inspected_index); 74 | 75 | if (x > 0 and neighbour_info_data[y][x - 1] != null) { 76 | const neighbour = neighbour_info_data[y][x - 1].?; 77 | inspected.allowed_neighbours.left.set(neighbour); 78 | } 79 | if (x < TileMapProto.width - 1 and neighbour_info_data[y][x + 1] != null) { 80 | const neighbour = neighbour_info_data[y][x + 1].?; 81 | inspected.allowed_neighbours.right.set(neighbour); 82 | } 83 | if (y > 0 and neighbour_info_data[y - 1][x] != null) { 84 | const neighbour = neighbour_info_data[y - 1][x].?; 85 | inspected.allowed_neighbours.top.set(neighbour); 86 | } 87 | if (y < TileMapProto.height - 1 and neighbour_info_data[y + 1][x] != null) { 88 | const neighbour = neighbour_info_data[y + 1][x].?; 89 | inspected.allowed_neighbours.bottom.set(neighbour); 90 | } 91 | } 92 | } 93 | } 94 | 95 | break :blk tile_info; 96 | } else |_| blk: { 97 | break :blk std.mem.zeroes([256]TileInfo); 98 | }; 99 | 100 | for (app.wave_function.tiles) |*row| { 101 | for (row) |*tile| { 102 | // everything seems possible 103 | tile.* = .{ .interposition = app.available_tiles }; 104 | } 105 | } 106 | } 107 | 108 | pub fn setupGraphics(app: *Application) !void { 109 | try app.rm.initializeGpuData(); 110 | } 111 | 112 | pub fn resize(app: *Application, width: u15, height: u15) !void { 113 | app.screen_size = zero_graphics.Size{ .width = width, .height = height }; 114 | } 115 | 116 | fn getTileSourceRectangle(index: u8) Rectangle { 117 | return Rectangle{ 118 | .x = Tile.width * @as(i16, index % 8), 119 | .y = Tile.height * @as(i16, index / 8), 120 | .width = Tile.width, 121 | .height = Tile.height, 122 | }; 123 | } 124 | 125 | fn getTileRectangle(x: usize, y: usize) Rectangle { 126 | return Rectangle{ 127 | .x = @intCast(i16, Tile.width * x), 128 | .y = @intCast(i16, Tile.height * y), 129 | .width = Tile.width, 130 | .height = Tile.height, 131 | }; 132 | } 133 | 134 | pub fn update(app: *Application) !bool { 135 | const tile_selection_rectangle = Rectangle{ 136 | .x = @intCast(i16, Tile.width * (TileMapProto.width + 1)), 137 | .y = 0, 138 | .width = 8 * Tile.width, 139 | .height = 8 * Tile.height, 140 | }; 141 | 142 | const tile_map_rectangle = Rectangle{ 143 | .x = 0, 144 | .y = 0, 145 | .width = Tile.width * TileMapProto.width, 146 | .height = Tile.height * TileMapProto.height, 147 | }; 148 | 149 | app.r2.reset(); 150 | 151 | while (app.input_queue.pollEvent()) |event| { 152 | switch (event) { 153 | .quit => return false, 154 | .pointer_motion => |pt| app.mouse_pos = pt, 155 | .pointer_press => |pointer| switch (pointer) { 156 | .primary => { 157 | if (tile_selection_rectangle.contains(app.mouse_pos)) { 158 | const tx = @intCast(u32, app.mouse_pos.x - tile_selection_rectangle.x) / Tile.width; 159 | const ty = @intCast(u32, app.mouse_pos.y - tile_selection_rectangle.y) / Tile.height; 160 | app.selected_draw_tile = @intCast(u8, 8 * ty + tx); 161 | } else { 162 | // 163 | } 164 | }, 165 | .secondary => {}, 166 | }, 167 | .pointer_release => {}, 168 | .text_input => {}, 169 | .key_down => |key| switch (key) { 170 | .f1 => app.mode = .tile_editor, 171 | .f2 => app.mode = .pattern_editor, 172 | .f6 => if (app.mode == .pattern_editor) { 173 | var file = try std.fs.cwd().createFile("source-tiles.dat", .{}); 174 | defer file.close(); 175 | 176 | try s2s.serialize(file.writer(), TileMap(?u8).Data, app.tile_map.tiles); 177 | 178 | std.log.info("saved", .{}); 179 | }, 180 | .space => { 181 | // collapse a single tile 182 | 183 | var open_set = std.AutoArrayHashMap(Coordinate, TileSet).init(app.allocator); 184 | defer open_set.deinit(); 185 | 186 | for (app.wave_function.tiles) |row, y| { 187 | for (row) |cell, x| { 188 | switch (cell) { 189 | .collapsed => {}, 190 | .interposition => try open_set.put(Coordinate{ .x = x, .y = y }, undefined), 191 | } 192 | } 193 | } 194 | 195 | if (open_set.count() > 0) { 196 | const initial_cell_pos = open_set.keys()[app.random.random().intRangeLessThan(usize, 0, open_set.count())]; 197 | 198 | const initial_cell = &app.wave_function.tiles[initial_cell_pos.y][initial_cell_pos.x]; 199 | std.debug.assert(initial_cell.* == .interposition); 200 | 201 | var collapse_options = std.BoundedArray(u8, 256){}; 202 | { 203 | var i: u32 = 0; 204 | while (i < 256) : (i += 1) { 205 | if (initial_cell.interposition.isSet(i)) { 206 | collapse_options.append(@truncate(u8, i)) catch unreachable; 207 | } 208 | } 209 | } 210 | if (collapse_options.len == 0) { 211 | std.log.info("invalid collapse option: {},{}", .{ initial_cell_pos.x, initial_cell_pos.y }); 212 | } else { 213 | const collapse_index = collapse_options.slice()[app.random.random().intRangeLessThan(usize, 0, collapse_options.len)]; 214 | 215 | try app.collapseCell(initial_cell_pos, collapse_index); 216 | } 217 | } 218 | }, 219 | else => {}, 220 | }, 221 | .key_up => {}, 222 | } 223 | } 224 | if (tile_map_rectangle.contains(app.mouse_pos)) { 225 | const tx = @intCast(u32, app.mouse_pos.x - tile_map_rectangle.x) / Tile.width; 226 | const ty = @intCast(u32, app.mouse_pos.y - tile_map_rectangle.y) / Tile.height; 227 | 228 | const visual_tile = &app.tile_map.tiles[ty][tx]; 229 | const wave_tile = &app.wave_function.tiles[ty][tx]; 230 | if (app.input_queue.mouse_state.get(.primary)) { 231 | switch (app.mode) { 232 | .pattern_editor => visual_tile.* = app.selected_draw_tile, 233 | .tile_editor => if (wave_tile.* == .interposition) { 234 | if (wave_tile.interposition.isSet(app.selected_draw_tile)) { 235 | try app.collapseCell( 236 | Coordinate{ .x = tx, .y = ty }, 237 | app.selected_draw_tile, 238 | ); 239 | } 240 | }, 241 | } 242 | } else if (app.input_queue.mouse_state.get(.secondary)) { 243 | switch (app.mode) { 244 | .pattern_editor => visual_tile.* = null, 245 | .tile_editor => {}, // wave_tile.* = .{ .interposition = app.available_tiles }; 246 | } 247 | } 248 | } 249 | switch (app.mode) { 250 | .tile_editor => { 251 | for (app.wave_function.tiles) |row, y| { 252 | for (row) |interposition, x| { 253 | const tile_rect = getTileRectangle(x, y); 254 | switch (interposition) { 255 | .collapsed => |index| { 256 | try app.r2.drawPartialTexture( 257 | tile_rect, 258 | app.tile_set_tex, 259 | getTileSourceRectangle(index), 260 | null, 261 | ); 262 | }, 263 | .interposition => |possibilities| { 264 | // 265 | 266 | var i: u32 = 0; 267 | while (i < 256) : (i += 1) { 268 | const index = @truncate(u8, i); 269 | 270 | if (possibilities.isSet(index)) { 271 | var smol_rect = tile_rect; 272 | smol_rect.width = 4; 273 | smol_rect.height = 4; 274 | smol_rect.x += smol_rect.width * (index % 8); 275 | smol_rect.y += smol_rect.height * (index / 8); 276 | 277 | try app.r2.fillRectangle(smol_rect, Color.lime); 278 | } 279 | } 280 | 281 | try app.r2.drawRectangle(tile_rect, Color.blue); 282 | }, 283 | } 284 | } 285 | } 286 | }, 287 | .pattern_editor => { 288 | for (app.tile_map.tiles) |row, y| { 289 | for (row) |maybe_tile_index, x| { 290 | if (maybe_tile_index) |index| { 291 | try app.r2.drawPartialTexture( 292 | getTileRectangle(x, y), 293 | app.tile_set_tex, 294 | getTileSourceRectangle(index), 295 | null, 296 | ); 297 | } 298 | } 299 | } 300 | }, 301 | } 302 | 303 | try app.r2.drawTexture( 304 | tile_selection_rectangle, 305 | app.tile_set_tex, 306 | null, 307 | ); 308 | 309 | try app.r2.drawRectangle( 310 | Rectangle{ 311 | .x = tile_selection_rectangle.x + Tile.width * (app.selected_draw_tile % 8), 312 | .y = tile_selection_rectangle.y + Tile.height * (app.selected_draw_tile / 8), 313 | .width = Tile.width, 314 | .height = Tile.height, 315 | }, 316 | Color.red, 317 | ); 318 | 319 | try app.r2.drawLine( 320 | app.mouse_pos.x - 10, 321 | app.mouse_pos.y, 322 | app.mouse_pos.x + 10, 323 | app.mouse_pos.y, 324 | Color.red, 325 | ); 326 | try app.r2.drawLine( 327 | app.mouse_pos.x, 328 | app.mouse_pos.y - 10, 329 | app.mouse_pos.x, 330 | app.mouse_pos.y + 10, 331 | Color.red, 332 | ); 333 | 334 | return true; 335 | } 336 | 337 | const Coordinate = struct { 338 | x: usize, 339 | y: usize, 340 | 341 | fn mutX(self: @This(), dx: i8) @This() { 342 | var copy = self; 343 | copy.x = @intCast(usize, @intCast(isize, copy.x) + dx); 344 | return copy; 345 | } 346 | fn mutY(self: @This(), dy: i8) @This() { 347 | var copy = self; 348 | copy.y = @intCast(usize, @intCast(isize, copy.y) + dy); 349 | return copy; 350 | } 351 | }; 352 | 353 | fn collapseCell(app: *Application, pos: Coordinate, tile_index: u8) !void { 354 | const initial_cell_pos = pos; 355 | const initial_cell = &app.wave_function.tiles[initial_cell_pos.y][initial_cell_pos.x]; 356 | 357 | if (initial_cell.* != .interposition) 358 | return error.AlreadyCollapsed; 359 | if (!initial_cell.interposition.isSet(tile_index)) 360 | return error.InvalidTileIndex; 361 | 362 | var open_set = std.AutoArrayHashMap(Coordinate, TileSet).init(app.allocator); 363 | defer open_set.deinit(); 364 | 365 | initial_cell.* = .{ .collapsed = tile_index }; 366 | 367 | var initial_info = tileSetForIndex(tile_index); 368 | 369 | try open_set.put(initial_cell_pos, initial_info); 370 | 371 | while (open_set.count() > 0) { 372 | const cell_position = open_set.keys()[0]; 373 | //const new_mask = open_set.values()[0]; 374 | _ = open_set.swapRemove(cell_position); 375 | 376 | const observed_cell = &app.wave_function.tiles[cell_position.y][cell_position.x]; 377 | 378 | const Pair = struct { 379 | pos: Coordinate, 380 | mask: TileSet, 381 | 382 | fn getNeighbourMask(info: [256]TileInfo, func: WaveFunction, comptime neighbour: []const u8) TileSet { 383 | switch (func) { 384 | .collapsed => |i| return @field(info[i].allowed_neighbours, neighbour), 385 | .interposition => |possibilities| { 386 | var result = TileSet.initEmpty(); 387 | 388 | var i: u32 = 0; 389 | while (i < 256) : (i += 1) { 390 | if (possibilities.isSet(i)) { 391 | result.setUnion(@field(info[i].allowed_neighbours, neighbour)); 392 | } 393 | } 394 | 395 | return result; 396 | }, 397 | } 398 | } 399 | }; 400 | 401 | var neighbours = std.BoundedArray(Pair, 4){}; 402 | { 403 | if (cell_position.x > 0) { 404 | try neighbours.append(Pair{ 405 | .pos = cell_position.mutX(-1), 406 | .mask = Pair.getNeighbourMask(app.tile_info, observed_cell.*, "left"), 407 | }); 408 | } 409 | if (cell_position.y > 0) { 410 | try neighbours.append(Pair{ 411 | .pos = cell_position.mutY(-1), 412 | .mask = Pair.getNeighbourMask(app.tile_info, observed_cell.*, "top"), 413 | }); 414 | } 415 | if (cell_position.x < TileMapProto.width - 1) { 416 | try neighbours.append(Pair{ 417 | .pos = cell_position.mutX(1), 418 | .mask = Pair.getNeighbourMask(app.tile_info, observed_cell.*, "right"), 419 | }); 420 | } 421 | if (cell_position.y < TileMapProto.height - 1) { 422 | try neighbours.append(Pair{ 423 | .pos = cell_position.mutY(1), 424 | .mask = Pair.getNeighbourMask(app.tile_info, observed_cell.*, "bottom"), 425 | }); 426 | } 427 | } 428 | 429 | std.log.info("observe {},{} with {} neighbours", .{ cell_position.x, cell_position.y, neighbours.len }); 430 | 431 | for (neighbours.slice()) |pair| { 432 | const neighbour_pos = pair.pos; 433 | const neighbour_cell = &app.wave_function.tiles[neighbour_pos.y][neighbour_pos.x]; 434 | 435 | if (neighbour_cell.* != .interposition) 436 | continue; 437 | 438 | const interposition = &neighbour_cell.interposition; 439 | 440 | const previous = interposition.*; 441 | 442 | interposition.setIntersection(pair.mask); 443 | 444 | const count = interposition.count(); 445 | 446 | if (previous.count() != count) { 447 | const new_state = interposition.*; 448 | 449 | if (count == 1) { 450 | // collapse cell 451 | 452 | neighbour_cell.* = .{ .collapsed = @intCast(u8, new_state.findFirstSet().?) }; 453 | } 454 | 455 | const gop = try open_set.getOrPut(neighbour_pos); 456 | if (gop.found_existing) { 457 | gop.value_ptr.setIntersection(new_state); 458 | } else { 459 | gop.value_ptr.* = new_state; 460 | } 461 | } else { 462 | // unchanged 463 | } 464 | } 465 | } 466 | } 467 | 468 | pub fn render(app: *Application) !void { 469 | zero_graphics.gles.clear(zero_graphics.gles.COLOR_BUFFER_BIT); 470 | 471 | app.r2.render(app.screen_size); 472 | } 473 | 474 | pub fn teardownGraphics(app: *Application) void { 475 | app.rm.destroyGpuData(); 476 | } 477 | 478 | pub fn deinit(app: *Application) void { 479 | app.r2.deinit(); 480 | app.rm.deinit(); 481 | } 482 | 483 | const Tile = struct { 484 | pub const width = 32; 485 | pub const height = 32; 486 | 487 | index: u8, 488 | }; 489 | 490 | const TileMapProto = struct { 491 | pub const width = 30; 492 | pub const height = 20; 493 | }; 494 | 495 | pub fn TileMap(comptime T: type) type { 496 | return struct { 497 | pub const Data = [TileMapProto.height][TileMapProto.width]T; 498 | pub const width = TileMapProto.width; 499 | pub const height = TileMapProto.height; 500 | 501 | tiles: Data = std.mem.zeroes(Data), 502 | }; 503 | } 504 | 505 | const TileSet = std.StaticBitSet(256); 506 | 507 | const TileNeighbourData = struct { 508 | top: TileSet = TileSet.initEmpty(), // y-1 509 | left: TileSet = TileSet.initEmpty(), // x-1 510 | right: TileSet = TileSet.initEmpty(), // x+1 511 | bottom: TileSet = TileSet.initEmpty(), // y+1 512 | }; 513 | 514 | const TileInfo = struct { 515 | allowed_neighbours: TileNeighbourData, 516 | }; 517 | 518 | const WaveFunction = union(enum) { 519 | collapsed: u8, 520 | interposition: TileSet, 521 | }; 522 | 523 | fn tileSetForIndex(index: u8) TileSet { 524 | var set = TileSet.initEmpty(); 525 | set.set(index); 526 | return set; 527 | } 528 | 529 | const Mode = enum { 530 | tile_editor, 531 | pattern_editor, 532 | }; 533 | --------------------------------------------------------------------------------