├── .github └── workflows │ └── build.yml ├── LICENSE.md ├── README.md ├── build.zig ├── build.zig.zon ├── doc └── demo.gif ├── httpserver ├── root.zig └── serve.zig └── src ├── cart.html ├── cart.wasm ├── clock.zig ├── config.zig ├── console.zig ├── display.zig ├── gamestate.zig ├── graph.zig ├── index.html ├── live.js ├── main.zig ├── record.zig ├── test.zig ├── ui.zig ├── uiagenthuman.zig ├── uiagentmachine.zig ├── uiagentrandom.zig ├── wasm4.css ├── wasm4.js ├── webmain.zig └── zoridor.js /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 6 | permissions: 7 | contents: read 8 | pages: write 9 | id-token: write 10 | 11 | 12 | jobs: 13 | test: 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, macos-latest] 17 | runs-on: ${{matrix.os}} 18 | steps: 19 | - name: Checkout zoridor 20 | uses: actions/checkout@v2 21 | with: 22 | path: zoridor 23 | - name: Setup Zig 24 | uses: mlugg/setup-zig@v1 25 | with: 26 | version: master 27 | - name: Build test 28 | run: zig build test 29 | working-directory: zoridor 30 | - name: Build terminal 31 | run: zig build -Dweb=false 32 | working-directory: zoridor 33 | - name: Build web 34 | run: zig build -Dweb=true 35 | working-directory: zoridor 36 | - name: Setup Pages 37 | if: github.ref == 'refs/heads/main' 38 | uses: actions/configure-pages@v3 39 | - name: Upload Artifact 40 | if: github.ref == 'refs/heads/main' 41 | uses: actions/upload-pages-artifact@v1 42 | with: 43 | path: "zoridor/zig-out" 44 | - name: Deploy to GitHub Pages 45 | id: deployment 46 | uses: actions/deploy-pages@v2 47 | 48 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Ringtail Software Ltd. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zoridor 2 | 3 | A terminal and web version of the [Quoridor](https://en.wikipedia.org/wiki/Quoridor) board game 4 | 5 | [WASM4](https://wasm4.org/) version on [wasm4 branch](https://github.com/ringtailsoftware/zoridor/tree/wasm4). 6 | 7 | Play on the web at https://ringtailsoftware.github.io/zoridor/ 8 | 9 | Or play the WASM4 cart at https://ringtailsoftware.github.io/zoridor/cart.html 10 | 11 | ![](doc/demo.gif) 12 | 13 | Quoridor tutorials: 14 | 15 | - https://www.youtube.com/watch?v=39T3L6hNfmg 16 | - https://www.youtube.com/watch?v=FDdm-EgRy9g 17 | - https://www.youtube.com/watch?v=6ISruhN0Hc0 18 | 19 | # Running 20 | 21 | Get [Zig](https://ziglang.org/download/) 22 | 23 | Terminal version: 24 | 25 | zig build run 26 | 27 | Web version: 28 | 29 | zig build -Dweb=true && zig build serve -- zig-out -p 8000 30 | 31 | Browse to http://localhost:8000 32 | 33 | # Development 34 | 35 | Auto-rebuild and reload on change 36 | 37 | watchexec -r --stop-signal SIGKILL -e zig,html,css,js,zon -w src 'zig build -Dweb=true && zig build serve -- zig-out -p 8000' 38 | 39 | # Terminal mode controls 40 | 41 | - You are Player 1. Your objective is to move your red pawn from the top of the board to the bottom of the board 42 | - Player 2 starts at the bottom and is attempting to reach the top of the board 43 | - On each turn you can either move your pawn or add one fence piece to the board (you have 10 to start with) 44 | 45 | ## Moving a pawn 46 | 47 | - Use the cursor keys to choose where to move to. Your pawn may only move one square on each turn and cannot move diagonally 48 | - The "[ ]" mark where your pawn will move and is coloured green for a valid move and red for invalid 49 | - Once you have selected a move, press enter to move the pawn 50 | 51 | ## Adding a fence 52 | 53 | - Press tab to switch from pawn to fence mode 54 | - Use the cursor keys to choose where to add the fence 55 | - Fences must not cross other fences, or completely block either player from reaching their goal. An invalid fence position will be shown in red 56 | - To rotate the fence, press space 57 | - Once you have positioned the fence, press enter to place it 58 | 59 | # Command line options 60 | 61 | Help 62 | 63 | zig build run -- -h 64 | 65 | To watch machine vs machine matches forever: 66 | 67 | zig build run -- -1 machine -2 machine -f 68 | 69 | On exit, a record of all moves is printed in both Glendenning format and base64. The base64 format can be reloaded with `zig build run -- -l jcNJujqxKRY2sA==` 70 | 71 | # Theory 72 | 73 | For a comprehensive examination of playing Quoridor, see [Lisa Glendenning's Thesis](https://www.labri.fr/perso/renault/working/teaching/projets/files/glendenning_ugrad_thesis.pdf) 74 | 75 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build) void { 4 | var options = b.addOptions(); 5 | const web = b.option(bool, "web", "Target web") orelse false; // -Dweb= 6 | options.addOption(bool, "web", web); 7 | 8 | const stdTarget = b.standardTargetOptions(.{}); 9 | const optimize = b.standardOptimizeOption(.{}); 10 | 11 | const webTarget = b.resolveTargetQuery(.{ 12 | .cpu_arch = .wasm32, 13 | .os_tag = .freestanding, 14 | }); 15 | 16 | const exe = b.addExecutable(.{ 17 | .name = "zoridor", 18 | .root_source_file = b.path(if (web) "src/webmain.zig" else "src/main.zig"), 19 | .target = if (web) webTarget else stdTarget, 20 | .optimize = optimize, 21 | }); 22 | 23 | b.installArtifact(exe); 24 | 25 | const run_cmd = b.addRunArtifact(exe); 26 | run_cmd.step.dependOn(b.getInstallStep()); 27 | 28 | // This allows the user to pass arguments to the application in the build 29 | // command itself, like this: `zig build run -- arg1 arg2 etc` 30 | if (b.args) |args| { 31 | run_cmd.addArgs(args); 32 | } 33 | 34 | if (web) { 35 | std.debug.print("Building for web\n", .{}); 36 | b.installFile("src/index.html", "index.html"); 37 | b.installFile("src/cart.html", "cart.html"); 38 | b.installFile("src/cart.wasm", "cart.wasm"); 39 | b.installFile("src/wasm4.css", "wasm4.css"); 40 | b.installFile("src/wasm4.js", "wasm4.js"); 41 | b.installFile("src/zoridor.js", "zoridor.js"); 42 | b.installFile("src/live.js", "live.js"); 43 | exe.rdynamic = true; 44 | } else { 45 | std.debug.print("Building for terminal (not web)\n", .{}); 46 | const mibu = b.dependency("mibu", .{ 47 | .target = stdTarget, 48 | .optimize = optimize, 49 | }); 50 | exe.root_module.addImport("mibu", mibu.module("mibu")); 51 | 52 | const yazap = b.dependency("yazap", .{}); 53 | exe.root_module.addImport("yazap", yazap.module("yazap")); 54 | 55 | const exe_unit_tests = b.addTest(.{ 56 | .root_source_file = b.path("src/test.zig"), 57 | .target = stdTarget, 58 | .optimize = optimize, 59 | }); 60 | exe_unit_tests.root_module.addImport("mibu", mibu.module("mibu")); 61 | exe_unit_tests.root_module.addOptions("buildopts", options); 62 | 63 | const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); 64 | 65 | const test_step = b.step("test", "Run unit tests"); 66 | test_step.dependOn(&run_exe_unit_tests.step); 67 | } 68 | 69 | // allow @import("buildopts") 70 | exe.root_module.addOptions("buildopts", options); 71 | 72 | const run_step = b.step("run", "Run the app"); 73 | run_step.dependOn(&run_cmd.step); 74 | 75 | 76 | // web server 77 | const serve_exe = b.addExecutable(.{ 78 | .name = "serve", 79 | .root_source_file = b.path("httpserver/serve.zig"), 80 | .target = stdTarget, 81 | .optimize = optimize, 82 | }); 83 | 84 | const mod_server = b.addModule("StaticHttpFileServer", .{ 85 | .root_source_file = b.path("httpserver/root.zig"), 86 | .target = stdTarget, 87 | .optimize = optimize, 88 | }); 89 | 90 | mod_server.addImport("mime", b.dependency("mime", .{ 91 | .target = stdTarget, 92 | .optimize = optimize, 93 | }).module("mime")); 94 | 95 | serve_exe.root_module.addImport("StaticHttpFileServer", mod_server); 96 | 97 | const run_serve_exe = b.addRunArtifact(serve_exe); 98 | if (b.args) |args| run_serve_exe.addArgs(args); 99 | 100 | const serve_step = b.step("serve", "Serve a directory of files"); 101 | serve_step.dependOn(&run_serve_exe.step); 102 | 103 | } 104 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | // This is the default name used by packages depending on this one. For 3 | // example, when a user runs `zig fetch --save `, this field is used 4 | // as the key in the `dependencies` table. Although the user can choose a 5 | // different name, most users will stick with this provided value. 6 | // 7 | // It is redundant to include "zig" in this name because it is already 8 | // within the Zig package namespace. 9 | .name = "zoridor", 10 | 11 | // This is a [Semantic Version](https://semver.org/). 12 | // In a future version of Zig it will be used for package deduplication. 13 | .version = "0.0.0", 14 | 15 | // This field is optional. 16 | // This is currently advisory only; Zig does not yet do anything 17 | // with this value. 18 | //.minimum_zig_version = "0.11.0", 19 | 20 | // This field is optional. 21 | // Each dependency must either provide a `url` and `hash`, or a `path`. 22 | // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. 23 | // Once all dependencies are fetched, `zig build` no longer requires 24 | // internet connectivity. 25 | .dependencies = .{ 26 | .mibu = .{ 27 | .url = "git+https://github.com/xyaman/mibu.git#b001662c929e2719ee24be585a3120640f946337", 28 | .hash = "1220d78664322b50e31a99cfb004b6fa60c43098d95abf7ec60a21ebeaf1c914edaf", 29 | }, 30 | .yazap = .{ 31 | .url = "git+https://github.com/prajwalch/yazap#c2e3122d5dd6192513ba590f229dbc535110efb8", 32 | .hash = "122054439ec36ac10987c87ae69f3b041b40b2e451af3fe3ef1fc578b3bad756a800", 33 | }, 34 | .StaticHttpFileServer = .{ 35 | .url = "git+https://github.com/andrewrk/StaticHttpFileServer.git#b65e1a27c9b2d4bb892e5ffd1a76715d6c0557ab", 36 | .hash = "1220db11bb50364857ec6047cfcdf0938dea6af3f24d360c6b6a6103364c8e353679", 37 | }, 38 | .mime = .{ 39 | .url = "https://github.com/andrewrk/mime/archive/refs/tags/2.0.1.tar.gz", 40 | .hash = "12209083b0c43d0f68a26a48a7b26ad9f93b22c9cff710c78ddfebb47b89cfb9c7a4", 41 | }, 42 | }, 43 | .paths = .{ 44 | "build.zig", 45 | "build.zig.zon", 46 | "src", 47 | // For example... 48 | //"LICENSE", 49 | //"README.md", 50 | }, 51 | } 52 | -------------------------------------------------------------------------------- /doc/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ringtailsoftware/zoridor/960df4f73236654226bf8fad1f578d379c6b23a8/doc/demo.gif -------------------------------------------------------------------------------- /httpserver/root.zig: -------------------------------------------------------------------------------- 1 | /// The key is index into backing_memory, where a HTTP request path is stored. 2 | files: File.Table, 3 | /// Stores file names relative to root directory and file contents, interleaved. 4 | bytes: std.ArrayListUnmanaged(u8), 5 | etag: []const u8, 6 | 7 | pub const File = struct { 8 | mime_type: mime.Type, 9 | name_start: usize, 10 | name_len: u16, 11 | /// Stored separately to make aliases work. 12 | contents_start: usize, 13 | contents_len: usize, 14 | 15 | pub const Table = std.HashMapUnmanaged( 16 | File, 17 | void, 18 | FileNameContext, 19 | std.hash_map.default_max_load_percentage, 20 | ); 21 | }; 22 | 23 | pub const Options = struct { 24 | allocator: std.mem.Allocator, 25 | /// Must have been opened with iteration permissions. 26 | root_dir: fs.Dir, 27 | cache_control_header: []const u8 = "max-age=0, must-revalidate", 28 | max_file_size: usize = std.math.maxInt(usize), 29 | /// Special alias "404" allows setting a particular file as the file sent 30 | /// for "not found" errors. If this alias is not provided, `serve` returns 31 | /// `error.FileNotFound` instead, leaving the response's state unmodified. 32 | aliases: []const Alias = &.{ 33 | .{ .request_path = "/", .file_path = "/index.html" }, 34 | .{ .request_path = "404", .file_path = "/404.html" }, 35 | }, 36 | ignoreFile: *const fn (path: []const u8) bool = &defaultIgnoreFile, 37 | etag: []const u8, 38 | 39 | pub const Alias = struct { 40 | request_path: []const u8, 41 | file_path: []const u8, 42 | }; 43 | 44 | }; 45 | 46 | pub const InitError = error{ 47 | OutOfMemory, 48 | InitFailed, 49 | }; 50 | 51 | pub fn init(options: Options) InitError!Server { 52 | const gpa = options.allocator; 53 | 54 | var it = try options.root_dir.walk(gpa); 55 | defer it.deinit(); 56 | 57 | var files: File.Table = .{}; 58 | errdefer files.deinit(gpa); 59 | 60 | var bytes: std.ArrayListUnmanaged(u8) = .{}; 61 | errdefer bytes.deinit(gpa); 62 | 63 | while (it.next() catch |err| { 64 | log.err("unable to scan root directory: {s}", .{@errorName(err)}); 65 | return error.InitFailed; 66 | }) |entry| { 67 | switch (entry.kind) { 68 | .file => { 69 | if (options.ignoreFile(entry.path)) continue; 70 | 71 | var file = options.root_dir.openFile(entry.path, .{}) catch |err| { 72 | log.err("unable to open '{s}': {s}", .{ entry.path, @errorName(err) }); 73 | return error.InitFailed; 74 | }; 75 | defer file.close(); 76 | 77 | const size = file.getEndPos() catch |err| { 78 | log.err("unable to stat '{s}': {s}", .{ entry.path, @errorName(err) }); 79 | return error.InitFailed; 80 | }; 81 | 82 | if (size > options.max_file_size) { 83 | log.err("file exceeds maximum size: '{s}'", .{entry.path}); 84 | return error.InitFailed; 85 | } 86 | 87 | const name_len = 1 + entry.path.len; 88 | try bytes.ensureUnusedCapacity(gpa, name_len + size); 89 | 90 | // Make the file system path identical independently of 91 | // operating system path inconsistencies. This converts 92 | // backslashes into forward slashes. 93 | const name_start = bytes.items.len; 94 | bytes.appendAssumeCapacity(canonical_sep); 95 | bytes.appendSliceAssumeCapacity(entry.path); 96 | if (fs.path.sep != canonical_sep) 97 | normalizePath(bytes.items[name_start..][0..name_len]); 98 | 99 | const contents_start = bytes.items.len; 100 | const contents_len = file.readAll(bytes.unusedCapacitySlice()) catch |e| { 101 | log.err("unable to read '{s}': {s}", .{ entry.path, @errorName(e) }); 102 | return error.InitFailed; 103 | }; 104 | if (contents_len != size) { 105 | log.err("unexpected EOF when reading '{s}'", .{entry.path}); 106 | return error.InitFailed; 107 | } 108 | bytes.items.len += contents_len; 109 | 110 | const ext = fs.path.extension(entry.basename); 111 | 112 | try files.putNoClobberContext(gpa, .{ 113 | .mime_type = mime.extension_map.get(ext) orelse .@"application/octet-stream", 114 | .name_start = name_start, 115 | .name_len = @intCast(name_len), 116 | .contents_start = contents_start, 117 | .contents_len = contents_len, 118 | }, {}, FileNameContext{ 119 | .bytes = bytes.items, 120 | }); 121 | }, 122 | else => continue, 123 | } 124 | } 125 | 126 | try files.ensureUnusedCapacityContext(gpa, @intCast(options.aliases.len), FileNameContext{ 127 | .bytes = bytes.items, 128 | }); 129 | 130 | for (options.aliases) |alias| { 131 | const file = files.getKeyAdapted(alias.file_path, FileNameAdapter{ 132 | .bytes = bytes.items, 133 | }) orelse { 134 | log.err("alias '{s}' points to nonexistent file '{s}'", .{ 135 | alias.request_path, alias.file_path, 136 | }); 137 | return error.InitFailed; 138 | }; 139 | 140 | const name_start = bytes.items.len; 141 | try bytes.appendSlice(gpa, alias.request_path); 142 | 143 | if (files.getOrPutAssumeCapacityContext(.{ 144 | .mime_type = file.mime_type, 145 | .name_start = name_start, 146 | .name_len = @intCast(alias.request_path.len), 147 | .contents_start = file.contents_start, 148 | .contents_len = file.contents_len, 149 | }, FileNameContext{ 150 | .bytes = bytes.items, 151 | }).found_existing) { 152 | log.err("alias '{s}'->'{s}' clobbers existing file or alias", .{ 153 | alias.request_path, alias.file_path, 154 | }); 155 | return error.InitFailed; 156 | } 157 | } 158 | 159 | return .{ 160 | .files = files, 161 | .bytes = bytes, 162 | .etag = options.etag, 163 | }; 164 | } 165 | 166 | pub fn deinit(s: *Server, allocator: std.mem.Allocator) void { 167 | s.files.deinit(allocator); 168 | s.bytes.deinit(allocator); 169 | s.* = undefined; 170 | } 171 | 172 | pub const ServeError = error{FileNotFound} || std.http.Server.Response.WriteError; 173 | 174 | pub fn serve(s: *Server, request: *std.http.Server.Request) ServeError!void { 175 | const path = request.head.target; 176 | const file_name_adapter: FileNameAdapter = .{ .bytes = s.bytes.items }; 177 | const file, const status: std.http.Status = b: { 178 | break :b .{ 179 | s.files.getKeyAdapted(path, file_name_adapter) orelse { 180 | break :b .{ 181 | s.files.getKeyAdapted(@as([]const u8, "404"), file_name_adapter) orelse 182 | return error.FileNotFound, 183 | .not_found, 184 | }; 185 | }, 186 | .ok, 187 | }; 188 | }; 189 | const content = s.bytes.items[file.contents_start..][0..file.contents_len]; 190 | 191 | return request.respond(content, .{ 192 | .status = status, 193 | .extra_headers = &.{ 194 | .{ .name = "content-type", .value = @tagName(file.mime_type) }, 195 | .{ .name = "Etag", .value = s.etag }, 196 | }, 197 | }); 198 | } 199 | 200 | pub fn defaultIgnoreFile(path: []const u8) bool { 201 | const basename = fs.path.basename(path); 202 | return std.mem.startsWith(u8, basename, ".") or 203 | std.mem.endsWith(u8, basename, "~"); 204 | } 205 | 206 | const Server = @This(); 207 | const mime = @import("mime"); 208 | const std = @import("std"); 209 | const fs = std.fs; 210 | const assert = std.debug.assert; 211 | const log = std.log.scoped(.@"static-http-files"); 212 | 213 | const canonical_sep = fs.path.sep_posix; 214 | 215 | fn normalizePath(bytes: []u8) void { 216 | assert(fs.path.sep != canonical_sep); 217 | std.mem.replaceScalar(u8, bytes, fs.path.sep, canonical_sep); 218 | } 219 | 220 | const FileNameContext = struct { 221 | bytes: []const u8, 222 | 223 | pub fn eql(self: @This(), a: File, b: File) bool { 224 | const a_name = self.bytes[a.name_start..][0..a.name_len]; 225 | const b_name = self.bytes[b.name_start..][0..b.name_len]; 226 | return std.mem.eql(u8, a_name, b_name); 227 | } 228 | 229 | pub fn hash(self: @This(), x: File) u64 { 230 | const name = self.bytes[x.name_start..][0..x.name_len]; 231 | return std.hash_map.hashString(name); 232 | } 233 | }; 234 | 235 | const FileNameAdapter = struct { 236 | bytes: []const u8, 237 | 238 | pub fn eql(self: @This(), a_name: []const u8, b: File) bool { 239 | const b_name = self.bytes[b.name_start..][0..b.name_len]; 240 | return std.mem.eql(u8, a_name, b_name); 241 | } 242 | 243 | pub fn hash(self: @This(), adapted_key: []const u8) u64 { 244 | _ = self; 245 | return std.hash_map.hashString(adapted_key); 246 | } 247 | }; 248 | -------------------------------------------------------------------------------- /httpserver/serve.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const StaticHttpFileServer = @import("StaticHttpFileServer"); 3 | 4 | var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){}; 5 | 6 | pub fn main() !void { 7 | var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator); 8 | defer arena_state.deinit(); 9 | const arena = arena_state.allocator(); 10 | const gpa = general_purpose_allocator.allocator(); 11 | 12 | const args = try std.process.argsAlloc(arena); 13 | 14 | var listen_port: u16 = 0; 15 | var opt_root_dir_path: ?[]const u8 = null; 16 | 17 | { 18 | var i: usize = 1; 19 | while (i < args.len) : (i += 1) { 20 | const arg = args[i]; 21 | if (std.mem.startsWith(u8, arg, "-")) { 22 | if (std.mem.eql(u8, arg, "-p")) { 23 | i += 1; 24 | if (i >= args.len) fatal("expected arg after '{s}'", .{arg}); 25 | listen_port = std.fmt.parseInt(u16, args[i], 10) catch |err| { 26 | fatal("unable to parse port '{s}': {s}", .{ args[i], @errorName(err) }); 27 | }; 28 | } else { 29 | fatal("unrecognized argument: '{s}'", .{arg}); 30 | } 31 | } else if (opt_root_dir_path == null) { 32 | opt_root_dir_path = arg; 33 | } else { 34 | fatal("unexpected positional argument: '{s}'", .{arg}); 35 | } 36 | } 37 | } 38 | 39 | const root_dir_path = opt_root_dir_path orelse fatal("missing root dir path", .{}); 40 | 41 | var root_dir = std.fs.cwd().openDir(root_dir_path, .{ .iterate = true }) catch |err| 42 | fatal("unable to open directory '{s}': {s}", .{ root_dir_path, @errorName(err) }); 43 | defer root_dir.close(); 44 | 45 | const aliases:[2]StaticHttpFileServer.Options.Alias = .{ 46 | .{ .request_path = "/", .file_path = "/index.html" }, 47 | .{ .request_path = "404", .file_path = "/index.html" }, 48 | }; 49 | 50 | var etag_buf:[32]u8 = undefined; 51 | 52 | var static_http_file_server = try StaticHttpFileServer.init(.{ 53 | .allocator = gpa, 54 | .root_dir = root_dir, 55 | .aliases = &aliases, 56 | .etag = try std.fmt.bufPrint(&etag_buf, "{d}", .{std.time.nanoTimestamp()}), 57 | }); 58 | defer static_http_file_server.deinit(gpa); 59 | 60 | const address = try std.net.Address.parseIp("127.0.0.1", listen_port); 61 | var http_server = try address.listen(.{ 62 | .reuse_address = true, 63 | }); 64 | const port = http_server.listen_address.in.getPort(); 65 | std.debug.print("Listening at http://127.0.0.1:{d}/\n", .{port}); 66 | 67 | var read_buffer: [8000]u8 = undefined; 68 | accept: while (true) { 69 | const connection = try http_server.accept(); 70 | defer connection.stream.close(); 71 | 72 | var server = std.http.Server.init(connection, &read_buffer); 73 | while (server.state == .ready) { 74 | var request = server.receiveHead() catch |err| { 75 | std.debug.print("error: {s}\n", .{@errorName(err)}); 76 | continue :accept; 77 | }; 78 | try static_http_file_server.serve(&request); 79 | } 80 | } 81 | } 82 | 83 | fn fatal(comptime format: []const u8, args: anytype) noreturn { 84 | std.debug.print(format ++ "\n", args); 85 | std.process.exit(1); 86 | } 87 | -------------------------------------------------------------------------------- /src/cart.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | WASM-4 Cart 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/cart.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ringtailsoftware/zoridor/960df4f73236654226bf8fad1f578d379c6b23a8/src/cart.wasm -------------------------------------------------------------------------------- /src/clock.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const buildopts = @import("buildopts"); 3 | 4 | extern fn getTimeUs() u32; 5 | 6 | var firstTime = true; 7 | var toff: i128 = 0; 8 | pub fn getTimeUs_native() u32 { 9 | if (firstTime) { 10 | firstTime = false; 11 | toff = std.time.nanoTimestamp(); 12 | } 13 | return @intCast(@mod(@divTrunc(std.time.nanoTimestamp() - toff, 1000), std.math.maxInt(u32))); 14 | } 15 | 16 | var startTime: u32 = 0; 17 | 18 | pub fn initTime() void { 19 | if (buildopts.web) { 20 | startTime = getTimeUs(); 21 | } else { 22 | startTime = getTimeUs_native(); 23 | } 24 | } 25 | 26 | pub fn millis() u32 { 27 | if (buildopts.web) { 28 | return (getTimeUs() - startTime) / 1000; 29 | } else { 30 | return (getTimeUs_native() - startTime) / 1000; 31 | } 32 | } 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/config.zig: -------------------------------------------------------------------------------- 1 | const mibu = @import("mibu"); 2 | const color = mibu.color; 3 | const yazap = @import("yazap"); // command line parsing 4 | const std = @import("std"); 5 | const UiAgent = @import("ui.zig").UiAgent; 6 | 7 | pub const GRIDSIZE: usize = 9; 8 | pub const NUM_PAWNS = 2; 9 | pub const NUM_FENCES = 20; 10 | pub const MAXMOVES = (5 * 5) + 2 * (9 - 1) * (9 - 1); // largest possible number of legal moves, pawnmoves + fencemoves 11 | pub const PAWN_EXPLORE_DIST = 2; // how many squares away to allow interactive exploring for pawn move 12 | pub const pawnColour = [NUM_PAWNS]color.Color{ .yellow, .magenta }; 13 | pub const fenceColour: color.Color = .white; 14 | pub const UI_XOFF = 3; 15 | pub const UI_YOFF = 2; 16 | pub const label_extra_w = 3; 17 | pub const COLUMN_LABEL_START = 'a'; 18 | pub const ROW_LABEL_START = '1'; 19 | 20 | pub var mini = false; 21 | 22 | pub var RANDOMSEED: ?u32 = null; // null = set from clock 23 | pub var RANDOMNESS: u32 = 0; 24 | 25 | pub var playForever = false; 26 | pub var players:[NUM_PAWNS]UiAgent = undefined; 27 | pub var b64GameStart:?[]u8 = null; 28 | 29 | // for holding last turn string 30 | pub var lastTurnBuf: [32]u8 = undefined; 31 | pub var lastTurnStr: ?[]u8 = null; 32 | pub var wins: [NUM_PAWNS]usize = .{ 0, 0 }; 33 | 34 | pub fn parseCommandLine() !void { 35 | const allocator = std.heap.page_allocator; 36 | const App = yazap.App; 37 | const Arg = yazap.Arg; 38 | 39 | var app = App.init(allocator, "zoridor", null); 40 | defer app.deinit(); 41 | 42 | var zoridor = app.rootCommand(); 43 | 44 | // find all available agent names 45 | var agentNames:[std.meta.fields(UiAgent).len][]const u8 = undefined; 46 | inline for (std.meta.fields(UiAgent), 0..) |f, i| { 47 | agentNames[i] = f.name; 48 | } 49 | 50 | const player1_opt = Arg.singleValueOptionWithValidValues("player1", '1', "Player 1 type", &agentNames); 51 | try zoridor.addArg(player1_opt); 52 | 53 | const player2_opt = Arg.singleValueOptionWithValidValues("player2", '2', "Player 2 type", &agentNames); 54 | try zoridor.addArg(player2_opt); 55 | 56 | var randseed_opt = Arg.singleValueOption("seedrand", 's', "Set random seed"); 57 | randseed_opt.setValuePlaceholder("12345"); 58 | try zoridor.addArg(randseed_opt); 59 | 60 | var randscore_opt = Arg.singleValueOption("randscore", 'r', "Set random move scoring value 0=same every time 100=random errors"); 61 | randscore_opt.setValuePlaceholder("0"); 62 | try zoridor.addArg(randscore_opt); 63 | 64 | const forever_opt = Arg.booleanOption("forever", 'f', "Play forever"); 65 | try zoridor.addArg(forever_opt); 66 | 67 | const mini_opt = Arg.booleanOption("mini", 'm', "Mini display < 80x24"); 68 | try zoridor.addArg(mini_opt); 69 | 70 | var load_opt = Arg.singleValueOption("load", 'l', "Load base64 game"); 71 | load_opt.setValuePlaceholder(""); 72 | try zoridor.addArg(load_opt); 73 | 74 | const matches = try app.parseProcess(); 75 | 76 | if (matches.containsArg("forever")) { 77 | playForever = true; 78 | } 79 | 80 | if (matches.containsArg("mini")) { 81 | mini = true; 82 | } 83 | 84 | if (matches.containsArg("load")) { 85 | if (matches.getSingleValue("load")) |b64str| { 86 | b64GameStart = try allocator.dupe(u8, b64str); 87 | } 88 | } 89 | 90 | if (matches.containsArg("randseed")) { 91 | if (matches.getSingleValue("randseed")) |seedStr| { 92 | const i = std.fmt.parseInt(u32, seedStr, 10) catch return; 93 | RANDOMSEED = i; 94 | } 95 | } 96 | 97 | if (matches.containsArg("randscore")) { 98 | if (matches.getSingleValue("randscore")) |randStr| { 99 | const i = std.fmt.parseInt(u32, randStr, 10) catch return; 100 | RANDOMNESS = i; 101 | } 102 | } 103 | 104 | if (matches.containsArg("player1")) { 105 | if (matches.getSingleValue("player1")) |typ| { 106 | players[0] = try UiAgent.make(typ); 107 | } 108 | } 109 | 110 | if (matches.containsArg("player2")) { 111 | if (matches.getSingleValue("player2")) |typ| { 112 | players[1] = try UiAgent.make(typ); 113 | } 114 | } 115 | } 116 | 117 | -------------------------------------------------------------------------------- /src/console.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | var cw = ConsoleWriter{}; 3 | const buildopts = @import("buildopts"); 4 | 5 | extern fn console_write(data: [*]const u8, len: usize) void; 6 | 7 | fn console_write_native(data: [*]const u8, len: usize) void { 8 | const stdout = std.io.getStdOut().writer(); 9 | _ = stdout.print("{s}", .{data[0..len]}) catch 0; 10 | } 11 | 12 | // Implement a std.io.Writer backed by console_write() 13 | const ConsoleWriter = struct { 14 | const Writer = std.io.Writer( 15 | *ConsoleWriter, 16 | error{}, 17 | write, 18 | ); 19 | 20 | fn write( 21 | self: *ConsoleWriter, 22 | data: []const u8, 23 | ) error{}!usize { 24 | _ = self; 25 | if (buildopts.web) { 26 | console_write(data.ptr, data.len); 27 | } else { 28 | console_write_native(data.ptr, data.len); 29 | } 30 | return data.len; 31 | } 32 | 33 | pub fn writer(self: *ConsoleWriter) Writer { 34 | return .{ .context = self }; 35 | } 36 | }; 37 | 38 | pub fn getWriter() *ConsoleWriter { 39 | return &cw; 40 | } 41 | -------------------------------------------------------------------------------- /src/display.zig: -------------------------------------------------------------------------------- 1 | // Raw character display, per character 2 | 3 | const std = @import("std"); 4 | const io = std.io; 5 | 6 | const mibu = @import("mibu"); 7 | const events = mibu.events; 8 | const term = mibu.term; 9 | const utils = mibu.utils; 10 | const color = mibu.color; 11 | const cursor = mibu.cursor; 12 | const style = mibu.style; 13 | 14 | const DISPLAYW = 80; 15 | const DISPLAYH = 29; 16 | 17 | pub const Display = struct { 18 | pub const DisplayPixel = struct { 19 | fg: color.Color, 20 | bg: color.Color, 21 | bold: bool, 22 | c: u8, 23 | }; 24 | 25 | const Self = @This(); 26 | const CLSPixel: DisplayPixel = .{ .fg = .white, .bg = .blue, .c = ' ', .bold = false }; 27 | raw_term: term.RawTerm, 28 | bufs: [2][DISPLAYW * DISPLAYH]DisplayPixel, // double buffer 29 | liveBufIndex: u1, 30 | offsBufIndex: u1, 31 | forceUpdate: bool, 32 | active: bool, 33 | 34 | pub fn init() !Self { 35 | const stdin = io.getStdIn(); 36 | const rt = try term.enableRawMode(stdin.handle); 37 | const writer = io.getStdOut().writer(); 38 | 39 | try cursor.hide(writer); 40 | 41 | return Self{ 42 | .raw_term = rt, 43 | .bufs = undefined, 44 | .liveBufIndex = 0, 45 | .offsBufIndex = 1, 46 | .forceUpdate = true, 47 | .active = true, 48 | }; 49 | } 50 | 51 | pub fn getSize() !term.TermSize { 52 | const stdin = io.getStdIn(); 53 | return term.getSize(stdin.handle); 54 | } 55 | 56 | pub fn destroy(self: *Self) void { 57 | // allow multiple calls to destroy 58 | if (self.active) { 59 | self.active = false; 60 | self.cls(); 61 | self.paint() catch {}; 62 | const writer = io.getStdOut().writer(); 63 | cursor.show(writer) catch {}; 64 | cursor.goTo(writer, 1, 1) catch {}; 65 | color.bg256(writer, .black) catch {}; 66 | color.fg256(writer, .white) catch {}; 67 | style.noBold(writer) catch {}; 68 | self.raw_term.disableRawMode() catch {}; 69 | } 70 | } 71 | 72 | pub fn getEvent(self: *Self, timeout: i32) !events.Event { 73 | _ = self; 74 | const stdin = io.getStdIn(); 75 | const next = try events.nextWithTimeout(stdin, timeout); 76 | return next; 77 | } 78 | 79 | pub fn setPixel(self: *Self, x: usize, y: usize, p: DisplayPixel) !void { 80 | self.bufs[self.liveBufIndex][y * DISPLAYW + x] = p; 81 | } 82 | 83 | pub fn cls(self: *Self) void { 84 | for (0..DISPLAYH) |y| { 85 | for (0..DISPLAYW) |x| { 86 | self.bufs[self.liveBufIndex][y * DISPLAYW + x] = CLSPixel; 87 | } 88 | } 89 | } 90 | 91 | pub fn paint(self: *Self) !void { 92 | // just draw changes to avoid sending excess data to terminal 93 | const writer = io.getStdOut().writer(); 94 | 95 | try writer.print("{s}", .{utils.comptimeCsi("?2026h", .{})}); 96 | 97 | for (0..DISPLAYH) |y| { 98 | for (0..DISPLAYW) |x| { 99 | const p = self.bufs[self.liveBufIndex][y * DISPLAYW + x]; 100 | const oldp = self.bufs[self.offsBufIndex][y * DISPLAYW + x]; 101 | if (self.forceUpdate or !std.meta.eql(p, oldp)) { 102 | try cursor.goTo(writer, x, y); 103 | try color.bg256(writer, p.bg); 104 | try color.fg256(writer, p.fg); 105 | if (p.bold) { 106 | try style.bold(writer); 107 | } else { 108 | try style.noBold(writer); 109 | } 110 | try writer.print("{c}", .{p.c}); 111 | self.bufs[self.offsBufIndex][y * DISPLAYW + x] = p; 112 | } 113 | } 114 | } 115 | 116 | try writer.print("{s}", .{utils.comptimeCsi("?2026l", .{})}); 117 | 118 | self.forceUpdate = false; 119 | 120 | // flip 121 | if (self.liveBufIndex == 0) { 122 | self.liveBufIndex = 1; 123 | self.offsBufIndex = 0; 124 | } else { 125 | self.liveBufIndex = 0; 126 | self.offsBufIndex = 1; 127 | } 128 | } 129 | }; 130 | -------------------------------------------------------------------------------- /src/gamestate.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const config = @import("config.zig"); 3 | const BitGraph = @import("graph.zig").BitGraph; 4 | 5 | pub const Pos = BitGraph.CoordPos; 6 | 7 | pub const Move = union(enum) { 8 | const Self = @This(); 9 | pawn: Pos, 10 | fence: PosDir, 11 | 12 | pub fn toString(self: Self, buf: []u8) ![]u8 { 13 | switch (self) { 14 | .pawn => |pawnmove| { 15 | return std.fmt.bufPrint(buf, "{c}{c}", .{ 16 | 'a' + @as(u8, @intCast(pawnmove.x)), 17 | '1' + @as(u8, @intCast(pawnmove.y)), 18 | }); 19 | }, 20 | .fence => |fencemove| { 21 | var d: u8 = 'v'; 22 | if (fencemove.dir == .horz) { 23 | d = 'h'; 24 | } 25 | return std.fmt.bufPrint(buf, "{c}{c}{c}", .{ 26 | 'a' + @as(u8, @intCast(fencemove.pos.x)), 27 | '1' + @as(u8, @intCast(fencemove.pos.y)), 28 | d, 29 | }); 30 | }, 31 | } 32 | } 33 | }; 34 | 35 | pub const VerifiedMove = struct { 36 | move: Move, 37 | legal: bool, 38 | }; 39 | 40 | pub const Dir = enum(u1) { 41 | vert = 0, 42 | horz = 1, 43 | 44 | pub fn flip(dir: Dir) Dir { 45 | switch (dir) { 46 | .vert => return .horz, 47 | .horz => return .vert, 48 | } 49 | } 50 | }; 51 | 52 | pub const PosDir = struct { 53 | pos: Pos, 54 | dir: Dir, 55 | }; 56 | 57 | pub const Pawn = struct { 58 | pos: Pos, 59 | goaly: usize, // end game line 60 | numFencesRemaining: usize, 61 | }; 62 | 63 | pub const PosPath = [BitGraph.MAXPATH]Pos; 64 | 65 | pub const GameState = struct { 66 | const Self = @This(); 67 | 68 | graph: BitGraph, 69 | pawns: [config.NUM_PAWNS]Pawn, 70 | fences: [config.NUM_FENCES]PosDir, 71 | numFences: usize, 72 | 73 | pub fn init() Self { 74 | var g = BitGraph.init(); 75 | g.addGridEdges(); 76 | 77 | return Self{ 78 | .graph = g, 79 | .pawns = .{ 80 | // pawn starting positions 81 | .{ .goaly = 8, .pos = .{ .x = 4, .y = 0 }, .numFencesRemaining = config.NUM_FENCES / 2 }, 82 | .{ .goaly = 0, .pos = .{ .x = 4, .y = 8 }, .numFencesRemaining = config.NUM_FENCES / 2 }, 83 | }, 84 | .numFences = 0, 85 | .fences = undefined, 86 | }; 87 | } 88 | 89 | fn isLegalMove(self: *const Self, pi: usize, move: Move) !bool { 90 | switch (move) { 91 | .pawn => |pawnmove| { 92 | return self.canMovePawn(pi, pawnmove); 93 | }, 94 | .fence => |fencemove| { 95 | return (self.pawns[pi].numFencesRemaining > 0) and self.canPlaceFence(fencemove); 96 | }, 97 | } 98 | } 99 | 100 | pub fn getFences(self: *const Self) []const PosDir { 101 | return self.fences[0..self.numFences]; 102 | } 103 | 104 | pub fn hasGameEnded(self: *const Self) bool { 105 | for (0..config.NUM_PAWNS) |i| { 106 | if (self.hasWon(i)) { 107 | return true; 108 | } 109 | } 110 | return false; 111 | } 112 | 113 | pub fn hasWon(self: *const Self, pi: usize) bool { 114 | return self.pawns[pi].pos.y == self.pawns[pi].goaly; 115 | } 116 | 117 | pub fn verifyMove(self: *const Self, pi: usize, move: Move) !VerifiedMove { 118 | return .{ 119 | .move = move, 120 | .legal = try self.isLegalMove(pi, move), 121 | }; 122 | } 123 | 124 | pub fn applyMove(self: *Self, pi: usize, vmove: VerifiedMove) !void { 125 | switch (vmove.move) { 126 | .pawn => |pawnmove| { 127 | self.movePawn(pi, pawnmove); 128 | }, 129 | .fence => |fencemove| { 130 | self.placeFence(pi, fencemove); 131 | }, 132 | } 133 | } 134 | 135 | pub fn getPawnPos(self: *const Self, pi: usize) Pos { 136 | return self.pawns[pi].pos; 137 | } 138 | 139 | pub fn getPawnGoal(self: *const Self, pi: usize) BitGraph.NodeIdRange { 140 | return .{ 141 | .min = BitGraph.coordPosToNodeId(.{ .x = 0, .y = @intCast(self.pawns[pi].goaly) }), 142 | .max = BitGraph.coordPosToNodeId(.{ .x = 8, .y = @intCast(self.pawns[pi].goaly) }), 143 | }; 144 | } 145 | 146 | fn rerouteAroundPawns(self: *const Self, pi: usize) BitGraph { 147 | var graph = self.graph; 148 | 149 | for (0..self.pawns.len) |i| { 150 | if (i != pi) { // ignore self 151 | // found an opponent pawn 152 | // if opp pawn is on our goal line, don't remove it - as it's legal to end game by jumping onto it 153 | if (self.pawns[i].pos.y != self.pawns[pi].goaly) { 154 | // clone graph 155 | // where opp pawn is, connect all of their outgoing edges to their incoming, so the node ceases to exist 156 | // this will enable jumping over it 157 | graph = BitGraph.clone(&graph); 158 | const ni = BitGraph.coordPosToNodeId(.{ .x = @intCast(self.pawns[i].pos.x), .y = @intCast(self.pawns[i].pos.y) }); 159 | graph.delNode(ni); 160 | } 161 | } 162 | } 163 | return graph; 164 | } 165 | 166 | pub fn getAllLegalMoves(self: *const Self, pi: usize, moves: *[config.MAXMOVES]Move, maxMoves:usize) !usize { 167 | // maxMoves = 0, generates all legal moves 168 | // Any other maxMoves value limits it 169 | var numMoves: usize = 0; 170 | 171 | // find all possible pawn moves -2 -> +2 around current pos 172 | var px = @as(isize, @intCast(self.pawns[pi].pos.x)) - 2; 173 | while (px <= self.pawns[pi].pos.x + 2) : (px += 1) { 174 | var py = @as(isize, @intCast(self.pawns[pi].pos.y)) - 2; 175 | while (py <= self.pawns[pi].pos.y + 2) : (py += 1) { 176 | if (px >= 0 and px < 9 and py >= 0 and py < 9) { // on grid 177 | const move = Move{ .pawn = .{ .x = @intCast(px), .y = @intCast(py) } }; 178 | const vm = try self.verifyMove(pi, move); 179 | if (vm.legal) { 180 | moves[numMoves] = move; 181 | numMoves += 1; 182 | if (maxMoves != 0 and numMoves >= maxMoves) { 183 | return numMoves; 184 | } 185 | } 186 | } 187 | } 188 | } 189 | 190 | // find all possible fence moves 191 | if (self.pawns[pi].numFencesRemaining > 0) { 192 | for (0..9 - 1) |fx| { 193 | for (0..9 - 1) |fy| { 194 | const movev = Move{ .fence = .{ .pos = .{ .x = @intCast(fx), .y = @intCast(fy) }, .dir = .vert } }; 195 | const moveh = Move{ .fence = .{ .pos = .{ .x = @intCast(fx), .y = @intCast(fy) }, .dir = .horz } }; 196 | const vmv = try self.verifyMove(pi, movev); 197 | const vmh = try self.verifyMove(pi, moveh); 198 | if (vmv.legal) { 199 | moves[numMoves] = movev; 200 | numMoves += 1; 201 | if (maxMoves != 0 and numMoves >= maxMoves) { 202 | return numMoves; 203 | } 204 | } 205 | if (vmh.legal) { 206 | moves[numMoves] = moveh; 207 | numMoves += 1; 208 | if (maxMoves != 0 and numMoves >= maxMoves) { 209 | return numMoves; 210 | } 211 | } 212 | } 213 | } 214 | } 215 | 216 | return numMoves; 217 | } 218 | 219 | pub fn canMovePawn(self: *const Self, pi: usize, targetPos: Pos) bool { 220 | std.debug.assert(targetPos.x < 9 and targetPos.y < 9); 221 | if (targetPos.x == self.pawns[pi].pos.x and targetPos.y == self.pawns[pi].pos.y) { // lands on self, no movement 222 | return false; 223 | } 224 | 225 | var graph = self.graph; 226 | 227 | for (0..self.pawns.len) |i| { 228 | if (i != pi) { // ignore self 229 | if (targetPos.x == self.pawns[i].pos.x and targetPos.y == self.pawns[i].pos.y) { 230 | // move will land on a pawn, not allowed, unless it's a winning move 231 | if (targetPos.y != self.pawns[pi].goaly) { 232 | return false; 233 | } 234 | } else { 235 | graph = self.rerouteAroundPawns(pi); 236 | } 237 | } 238 | } 239 | 240 | const a = BitGraph.CoordPos{ .x = @intCast(self.pawns[pi].pos.x), .y = @intCast(self.pawns[pi].pos.y) }; 241 | const b = BitGraph.CoordPos{ .x = @intCast(targetPos.x), .y = @intCast(targetPos.y) }; 242 | 243 | return graph.hasCoordEdgeUni(a, b); 244 | } 245 | 246 | pub fn movePawn(self: *Self, pi: usize, targetPos: Pos) void { 247 | std.debug.assert(self.canMovePawn(pi, targetPos)); 248 | self.pawns[pi].pos = targetPos; 249 | } 250 | 251 | pub fn canPlaceFence(self: *const Self, pd: PosDir) bool { 252 | // To place a fence: 253 | // - it must cut two existing edges in graph 254 | // a-b 255 | // | | 256 | // c-d 257 | 258 | const a = BitGraph.CoordPos{ .x = @intCast(pd.pos.x), .y = @intCast(pd.pos.y) }; 259 | const b = BitGraph.CoordPos{ .x = @intCast(pd.pos.x + 1), .y = @intCast(pd.pos.y) }; 260 | const c = BitGraph.CoordPos{ .x = @intCast(pd.pos.x), .y = @intCast(pd.pos.y + 1) }; 261 | const d = BitGraph.CoordPos{ .x = @intCast(pd.pos.x + 1), .y = @intCast(pd.pos.y + 1) }; 262 | 263 | // (if uni edge exists, then it's also bi-directionally connected) 264 | 265 | switch (pd.dir) { 266 | .vert => { 267 | if (!(self.graph.hasCoordEdgeUni(a, b) and self.graph.hasCoordEdgeUni(c, d))) { 268 | return false; 269 | } 270 | }, 271 | .horz => { 272 | if (!(self.graph.hasCoordEdgeUni(a, c) and self.graph.hasCoordEdgeUni(b, d))) { 273 | return false; 274 | } 275 | }, 276 | } 277 | 278 | // - must not hit any existing fences 279 | for (self.fences[0..self.numFences]) |f| { 280 | if (f.pos.x == pd.pos.x and f.pos.y == pd.pos.y) { 281 | return false; 282 | } 283 | 284 | // check if fence extents overlap when aligned 285 | if (f.dir == pd.dir) { 286 | switch (f.dir) { 287 | .vert => { 288 | if (f.pos.x == pd.pos.x and @abs(@as(isize, @intCast(f.pos.y)) - @as(isize, @intCast(pd.pos.y))) < 2) { 289 | return false; 290 | } 291 | }, 292 | .horz => { 293 | if (f.pos.y == pd.pos.y and @abs(@as(isize, @intCast(f.pos.x)) - @as(isize, @intCast(pd.pos.x))) < 2) { 294 | return false; 295 | } 296 | }, 297 | } 298 | } 299 | } 300 | 301 | // - it must not stop any pawn from having a path to goal 302 | for (self.pawns, 0..) |pawn, pi| { 303 | const start = BitGraph.coordPosToNodeId(BitGraph.CoordPos{ .x = @intCast(pawn.pos.x), .y = @intCast(pawn.pos.y) }); 304 | var pathbuf: [BitGraph.MAXPATH]BitGraph.NodeId = undefined; 305 | 306 | var gs = self.*; // clone gamestate 307 | placeFenceToGraph(&gs.graph, pd); // place fence 308 | var g = gs.rerouteAroundPawns(pi); // make graph skip over pawn locations 309 | if (null == g.findShortestPath(start, gs.getPawnGoal(pi), &pathbuf, true)) { // look for any path to goal 310 | return false; 311 | } 312 | } 313 | 314 | return true; 315 | } 316 | 317 | pub fn findShortestPath(self: *const Self, pi: usize, start: Pos, posPathBuf: *PosPath) ?[]Pos { 318 | const g = self.rerouteAroundPawns(pi); // make graph skip over pawn locations 319 | var nodePathBuf: [BitGraph.MAXPATH]BitGraph.NodeId = undefined; 320 | const nodePathO = g.findShortestPath(BitGraph.coordPosToNodeId(start), self.getPawnGoal(pi), &nodePathBuf, false); 321 | 322 | if (nodePathO) |nodePath| { 323 | var posPathLen: usize = 0; 324 | for (nodePath[1..nodePath.len]) |n| { // skip first element, as it's starting node 325 | posPathBuf[posPathLen] = BitGraph.nodeIdToCoordPos(n); 326 | posPathLen += 1; 327 | } 328 | return posPathBuf[0..posPathLen]; 329 | } else { 330 | return null; 331 | } 332 | } 333 | 334 | pub fn placeFence(self: *Self, pi: usize, pd: PosDir) void { 335 | std.debug.assert(self.canPlaceFence(pd)); 336 | std.debug.assert(self.numFences < config.NUM_FENCES); 337 | placeFenceToGraph(&self.graph, pd); 338 | self.fences[self.numFences] = pd; 339 | self.numFences += 1; 340 | self.pawns[pi].numFencesRemaining -= 1; 341 | } 342 | 343 | fn placeFenceToGraph(graph: *BitGraph, pd: PosDir) void { 344 | const a = BitGraph.CoordPos{ .x = @intCast(pd.pos.x), .y = @intCast(pd.pos.y) }; 345 | const b = BitGraph.CoordPos{ .x = @intCast(pd.pos.x + 1), .y = @intCast(pd.pos.y) }; 346 | const c = BitGraph.CoordPos{ .x = @intCast(pd.pos.x), .y = @intCast(pd.pos.y + 1) }; 347 | const d = BitGraph.CoordPos{ .x = @intCast(pd.pos.x + 1), .y = @intCast(pd.pos.y + 1) }; 348 | 349 | switch (pd.dir) { 350 | .vert => { 351 | graph.delCoordEdgeBi(a, b); 352 | graph.delCoordEdgeBi(c, d); 353 | }, 354 | .horz => { 355 | graph.delCoordEdgeBi(a, c); 356 | graph.delCoordEdgeBi(b, d); 357 | }, 358 | } 359 | } 360 | 361 | pub fn print(self: *const Self) void { 362 | self.graph.print(); 363 | } 364 | }; 365 | -------------------------------------------------------------------------------- /src/graph.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const buildopts = @import("buildopts"); 3 | 4 | pub const BitGraph = struct { 5 | pub const NodeId = u8; // 0 <= n < 81 6 | pub const Coord = u4; // 0 <= n < 9 7 | pub const CoordPos = struct { x: Coord, y: Coord }; 8 | pub const NodeIdRange = struct { // a range of continuous node ids inclusive of min and max 9 | min: NodeId, 10 | max: NodeId, 11 | }; 12 | pub const MAXPATH = 9 * 9; 13 | const Self = @This(); 14 | 15 | bitMatrix: [9 * 9]u128, // 81 lines, each using first 81 bits 16 | 17 | pub fn init() Self { 18 | return Self{ 19 | .bitMatrix = std.mem.zeroes([9 * 9]u128), 20 | }; 21 | } 22 | 23 | pub fn clone(other: *const Self) Self { 24 | return other.*; 25 | } 26 | 27 | pub fn delNode(self: *Self, dn: NodeId) void { 28 | // disconnect/orphan node, routing around it in graph 29 | 30 | // contruct list in outgoing of everything dn is currently connected to 31 | var outgoing: [9 * 9]NodeId = undefined; 32 | var numOutgoing: usize = 0; 33 | 34 | for (0..9 * 9) |n| { // could look at a smaller set, but should be quite fast 35 | if (dn != n) { 36 | if (self.hasEdgeUni(dn, @as(NodeId, @intCast(n)))) { 37 | outgoing[numOutgoing] = @intCast(n); 38 | numOutgoing += 1; 39 | } 40 | } 41 | } 42 | 43 | // for every node, if it did connect to dn, reroute to dn's children 44 | for (0..9 * 9) |n| { 45 | if (dn != n) { // not self 46 | if (self.hasEdgeUni(@intCast(n), @as(NodeId, @intCast(dn)))) { 47 | for (outgoing[0..numOutgoing]) |outn| { 48 | if (n != outn) { // don't connect nodes to selves 49 | self.addEdgeBi(@intCast(n), outn); 50 | } 51 | } 52 | self.delEdgeBi(dn, @intCast(n)); // remove old edge to dn 53 | } 54 | } 55 | } 56 | } 57 | 58 | // add a bidirectional edge 59 | pub fn addEdgeBi(self: *Self, n1: NodeId, n2: NodeId) void { 60 | std.debug.assert(n1 < 9 * 9); 61 | std.debug.assert(n2 < 9 * 9); 62 | std.debug.assert(n1 != n2); 63 | self.bitMatrix[n1] |= @as(u128, 1) << @as(u7, @intCast(n2)); 64 | self.bitMatrix[n2] |= @as(u128, 1) << @as(u7, @intCast(n1)); 65 | } 66 | 67 | // delete a bidirectional edge 68 | pub fn delEdgeBi(self: *Self, n1: NodeId, n2: NodeId) void { 69 | std.debug.assert(n1 < 9 * 9); 70 | std.debug.assert(n2 < 9 * 9); 71 | std.debug.assert(n1 != n2); 72 | self.bitMatrix[n1] &= ~(@as(u128, 1) << @as(u7, @intCast(n2))); 73 | self.bitMatrix[n2] &= ~(@as(u128, 1) << @as(u7, @intCast(n1))); 74 | } 75 | 76 | pub fn hasAnyEdges(self: *const Self, n1: NodeId) bool { 77 | std.debug.assert(n1 < 9 * 9); 78 | return self.bitMatrix[n1] != 0; 79 | } 80 | 81 | pub fn hasEdgeUni(self: *const Self, n1: NodeId, n2: NodeId) bool { 82 | std.debug.assert(n1 < 9 * 9); 83 | std.debug.assert(n2 < 9 * 9); 84 | std.debug.assert(n1 != n2); 85 | return self.bitMatrix[n1] & @as(u128, 1) << @as(u7, @intCast(n2)) > 0; 86 | } 87 | 88 | pub fn coordPosToNodeId(p: CoordPos) NodeId { 89 | std.debug.assert(p.x < 9); 90 | std.debug.assert(p.y < 9); 91 | return @as(NodeId, @intCast(p.y)) * 9 + @as(NodeId, @intCast(p.x)); 92 | } 93 | 94 | pub fn nodeIdToCoordPos(n: NodeId) CoordPos { 95 | std.debug.assert(n < 9 * 9); 96 | const y = n / 9; 97 | const x = n - (y * 9); 98 | return .{ .x = @intCast(x), .y = @intCast(y) }; 99 | } 100 | 101 | pub fn addCoordEdgeBi(self: *Self, a: CoordPos, b: CoordPos) void { 102 | self.addEdgeBi(coordPosToNodeId(a), coordPosToNodeId(b)); 103 | } 104 | 105 | pub fn hasCoordEdgeUni(self: *const Self, a: CoordPos, b: CoordPos) bool { 106 | return self.hasEdgeUni(coordPosToNodeId(a), coordPosToNodeId(b)); 107 | } 108 | 109 | pub fn delCoordEdgeBi(self: *Self, a: CoordPos, b: CoordPos) void { 110 | self.delEdgeBi(coordPosToNodeId(a), coordPosToNodeId(b)); 111 | } 112 | 113 | pub fn addGridEdges(self: *Self) void { 114 | // all bi-directional links between all orthogonal nodes on grid 115 | for (0..9) |y| { 116 | for (0..9) |x| { 117 | // many edges being repeatedly added, slow but harmless 118 | // left 119 | if (x > 0) { 120 | self.addCoordEdgeBi(.{ .x = @intCast(x), .y = @intCast(y) }, .{ .x = @intCast(x - 1), .y = @intCast(y) }); 121 | } 122 | // right 123 | if (x < 8) { 124 | self.addCoordEdgeBi(.{ .x = @intCast(x), .y = @intCast(y) }, .{ .x = @intCast(x + 1), .y = @intCast(y) }); 125 | } 126 | // up 127 | if (y > 0) { 128 | self.addCoordEdgeBi(.{ .x = @intCast(x), .y = @intCast(y) }, .{ .x = @intCast(x), .y = @intCast(y - 1) }); 129 | } 130 | // down 131 | if (y < 8) { 132 | self.addCoordEdgeBi(.{ .x = @intCast(x), .y = @intCast(y) }, .{ .x = @intCast(x), .y = @intCast(y + 1) }); 133 | } 134 | } 135 | } 136 | } 137 | 138 | const NodeData = struct { 139 | parent: NodeId, 140 | pathCost: ?u8, 141 | }; 142 | 143 | fn lessThan(context: *[9*9]NodeData, a: NodeId, b: NodeId) std.math.Order { 144 | return std.math.order(context[a].pathCost.?, context[b].pathCost.?); 145 | } 146 | 147 | pub fn findShortestPath(self: *const Self, start: NodeId, goal: NodeIdRange, path: *[MAXPATH]NodeId, anyPath: bool) ?[]NodeId { 148 | // find shortest path from start to any node in goal range, returning slice of NodeIds using path as buffer 149 | // if anyPath is set, exit with non-null result if goal is ever reached 150 | 151 | var nodes: [9 * 9]NodeData = undefined; 152 | for (0..9 * 9) |i| { 153 | nodes[i] = .{ 154 | .pathCost = null, // unknown 155 | .parent = undefined, 156 | }; 157 | } 158 | 159 | // use a fixed size buffer 160 | var buffer: [4096]u8 = undefined; // this size is a guess 161 | var fba = std.heap.FixedBufferAllocator.init(&buffer); 162 | const allocator = fba.allocator(); 163 | const pqlt = std.PriorityQueue(NodeId, *[9*9]NodeData, lessThan); 164 | 165 | // stack of nodes to expand 166 | var q = pqlt.init(allocator, &nodes); 167 | defer q.deinit(); 168 | 169 | nodes[start].pathCost = 0; 170 | q.add(start) catch {}; 171 | 172 | outer: while (q.count() > 0) { 173 | const n = q.remove(); // new pos to explore 174 | 175 | // for everything n is connected to 176 | if (self.hasAnyEdges(n)) { 177 | for (0..9 * 9) |n2| { 178 | if (n != n2) { 179 | if (self.hasEdgeUni(n, @as(NodeId, @intCast(n2)))) { 180 | // n has edge to n2 181 | var doExplore = false; 182 | if (nodes[n2].pathCost) |existingCost| { 183 | if (nodes[n].pathCost.? + 1 < existingCost) { 184 | doExplore = true; 185 | } 186 | } else { // unvisited node 187 | doExplore = true; 188 | } 189 | if (doExplore) { 190 | nodes[n2].pathCost = nodes[n].pathCost.? + 1; 191 | nodes[n2].parent = n; 192 | 193 | // push 194 | q.add(@as(NodeId, @intCast(n2))) catch {}; 195 | } 196 | 197 | if (anyPath) { 198 | if (n2 >= goal.min and n2 <= goal.max) { 199 | continue :outer; 200 | } 201 | } 202 | } 203 | } 204 | } 205 | } 206 | } 207 | 208 | // all discovered path costs 209 | // for (0..9) |y| { 210 | // for (0..9) |x| { 211 | // if (nodes[y*9+x].pathCost) |cost| { 212 | // std.debug.print("{d:0>2} ", .{cost}); 213 | // } else { 214 | // std.debug.print("XX ", .{}); 215 | // } 216 | // } 217 | // std.debug.print("\r\n", .{}); 218 | // } 219 | // std.debug.print("\r\n", .{}); 220 | //self.print(); 221 | //self.printEdges(); 222 | 223 | // find cheapest node on the goal line, then work backwards from there to find path 224 | var bestPathCost: usize = undefined; 225 | var bestGoal: ?NodeId = null; 226 | var first = true; 227 | for (goal.min..goal.max + 1) |g| { 228 | if (nodes[g].pathCost) |pathCost| { 229 | if (first or pathCost < bestPathCost) { 230 | first = false; 231 | bestGoal = @as(NodeId, @intCast(g)); 232 | bestPathCost = pathCost; 233 | } 234 | } 235 | } 236 | 237 | if (bestGoal) |g| { 238 | // std.debug.print("{any} -> {any} ({any})\r\n", .{start, g, nodes[g].pathCost.?}); 239 | // work backwards from the the target until reaching the root 240 | const pathLen = nodes[g].pathCost.?; 241 | path[nodes[g].pathCost.?] = g; 242 | var cur = g; 243 | while (true) { 244 | const n = nodes[cur]; 245 | path[n.pathCost.?] = cur; 246 | if (n.pathCost == 0) { // reached starting node 247 | break; 248 | } 249 | cur = n.parent; 250 | } 251 | return path[0 .. pathLen + 1]; 252 | } else { 253 | // goal unreachable 254 | return null; 255 | } 256 | } 257 | 258 | pub fn printEdges(self: *const Self) void { 259 | if (buildopts.web) { 260 | return; 261 | } 262 | for (0..9 * 9) |i| { 263 | std.debug.print("{d} => ", .{i}); 264 | for (0..9 * 9) |j| { 265 | if (i != j) { 266 | if (self.hasEdgeUni(@intCast(i), @intCast(j))) { 267 | std.debug.print("{d} ", .{j}); 268 | } 269 | } 270 | } 271 | std.debug.print("\r\n", .{}); 272 | } 273 | std.debug.print("\r\n", .{}); 274 | } 275 | 276 | pub fn print(self: *const Self) void { 277 | if (buildopts.web) { 278 | return; 279 | } 280 | 281 | // 00<>01< 02 282 | // ^V ^V V 283 | // 03<>04<>05 284 | std.debug.print("\r\n", .{}); 285 | 286 | for (0..9) |y| { 287 | for (0..9) |x| { 288 | std.debug.print("{d:0>2}", .{coordPosToNodeId(.{ .x = @intCast(x), .y = @intCast(y) })}); 289 | const me = CoordPos{ .x = @intCast(x), .y = @intCast(y) }; 290 | if (x < 8) { 291 | const right = CoordPos{ .x = @intCast(x + 1), .y = @intCast(y) }; 292 | if (self.hasCoordEdgeUni(right, me)) { 293 | std.debug.print("<", .{}); 294 | } else { 295 | std.debug.print(" ", .{}); 296 | } 297 | 298 | if (self.hasCoordEdgeUni(me, right)) { 299 | std.debug.print(">", .{}); 300 | } else { 301 | std.debug.print(" ", .{}); 302 | } 303 | } 304 | } 305 | std.debug.print("\r\n", .{}); 306 | if (y < 8) { 307 | for (0..9) |x| { 308 | const me = CoordPos{ .x = @intCast(x), .y = @intCast(y) }; 309 | const down = CoordPos{ .x = @intCast(x), .y = @intCast(y + 1) }; 310 | if (self.hasCoordEdgeUni(me, down)) { 311 | std.debug.print("V", .{}); 312 | } else { 313 | std.debug.print(" ", .{}); 314 | } 315 | if (self.hasCoordEdgeUni(down, me)) { 316 | std.debug.print("^", .{}); 317 | } else { 318 | std.debug.print(" ", .{}); 319 | } 320 | std.debug.print(" ", .{}); 321 | } 322 | } 323 | std.debug.print("\r\n", .{}); 324 | } 325 | std.debug.print("\r\n", .{}); 326 | } 327 | }; 328 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Zoridor 6 | 7 | 97 | 98 | 99 | 124 | 125 | 126 |

Zoridor

127 |

128 | Click a nearby empty square to move nearer to the opposite side, or click between the squares to place a wall and block your opponent's path. 129 | How to play 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 | All moves are recorded in the Game Log below. To save or load a game, copy and paste into the box then start the game. 163 |

164 |

165 |

166 |

167 | 168 |

169 |

170 | 171 |

172 |

173 | Game Log: 174 |

175 |
176 |

177 |
178 |
179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /src/live.js: -------------------------------------------------------------------------------- 1 | /* 2 | Live.js - One script closer to Designing in the Browser 3 | Written for Handcraft.com by Martin Kool (@mrtnkl). 4 | 5 | Version 4. 6 | Recent change: Made stylesheet and mimetype checks case insensitive. 7 | 8 | http://livejs.com 9 | http://livejs.com/license (MIT) 10 | @livejs 11 | 12 | Include live.js#css to monitor css changes only. 13 | Include live.js#js to monitor js changes only. 14 | Include live.js#html to monitor html changes only. 15 | Mix and match to monitor a preferred combination such as live.js#html,css 16 | 17 | By default, just include live.js to monitor all css, js and html changes. 18 | 19 | Live.js can also be loaded as a bookmarklet. It is best to only use it for CSS then, 20 | as a page reload due to a change in html or css would not re-include the bookmarklet. 21 | To monitor CSS and be notified that it has loaded, include it as: live.js#css,notify 22 | */ 23 | (function () { 24 | 25 | var headers = { "Etag": 1, "Last-Modified": 1, "Content-Length": 1, "Content-Type": 1 }, 26 | resources = {}, 27 | pendingRequests = {}, 28 | currentLinkElements = {}, 29 | oldLinkElements = {}, 30 | interval = 1000, 31 | loaded = false, 32 | active = { "html": 1, "css": 1, "js": 1 }; 33 | 34 | var Live = { 35 | 36 | // performs a cycle per interval 37 | heartbeat: function () { 38 | if (document.body) { 39 | // make sure all resources are loaded on first activation 40 | if (!loaded) Live.loadresources(); 41 | Live.checkForChanges(); 42 | } 43 | setTimeout(Live.heartbeat, interval); 44 | }, 45 | 46 | // loads all local css and js resources upon first activation 47 | loadresources: function () { 48 | 49 | // helper method to assert if a given url is local 50 | function isLocal(url) { 51 | var loc = document.location, 52 | reg = new RegExp("^\\.|^\/(?!\/)|^[\\w]((?!://).)*$|" + loc.protocol + "//" + loc.host); 53 | return url.match(reg); 54 | } 55 | 56 | // gather all resources 57 | var scripts = document.getElementsByTagName("script"), 58 | links = document.getElementsByTagName("link"), 59 | uris = []; 60 | 61 | // track local js urls 62 | for (var i = 0; i < scripts.length; i++) { 63 | var script = scripts[i], src = script.getAttribute("src"); 64 | if (src && isLocal(src)) 65 | uris.push(src); 66 | if (src && src.match(/\blive.js#/)) { 67 | for (var type in active) 68 | active[type] = src.match("[#,|]" + type) != null 69 | if (src.match("notify")) 70 | alert("Live.js is loaded."); 71 | } 72 | } 73 | if (!active.js) uris = []; 74 | if (active.html) uris.push(document.location.href); 75 | 76 | // track local css urls 77 | for (var i = 0; i < links.length && active.css; i++) { 78 | var link = links[i], rel = link.getAttribute("rel"), href = link.getAttribute("href", 2); 79 | if (href && rel && rel.match(new RegExp("stylesheet", "i")) && isLocal(href)) { 80 | uris.push(href); 81 | currentLinkElements[href] = link; 82 | } 83 | } 84 | 85 | // initialize the resources info 86 | for (var i = 0; i < uris.length; i++) { 87 | var url = uris[i]; 88 | Live.getHead(url, function (url, info) { 89 | resources[url] = info; 90 | }); 91 | } 92 | 93 | // add rule for morphing between old and new css files 94 | var head = document.getElementsByTagName("head")[0], 95 | style = document.createElement("style"), 96 | rule = "transition: all .3s ease-out;" 97 | css = [".livejs-loading * { ", rule, " -webkit-", rule, "-moz-", rule, "-o-", rule, "}"].join(''); 98 | style.setAttribute("type", "text/css"); 99 | head.appendChild(style); 100 | style.styleSheet ? style.styleSheet.cssText = css : style.appendChild(document.createTextNode(css)); 101 | 102 | // yep 103 | loaded = true; 104 | }, 105 | 106 | // check all tracking resources for changes 107 | checkForChanges: function () { 108 | for (var url in resources) { 109 | if (pendingRequests[url]) 110 | continue; 111 | 112 | Live.getHead(url, function (url, newInfo) { 113 | var oldInfo = resources[url], 114 | hasChanged = false; 115 | resources[url] = newInfo; 116 | for (var header in oldInfo) { 117 | // do verification based on the header type 118 | var oldValue = oldInfo[header], 119 | newValue = newInfo[header], 120 | contentType = newInfo["Content-Type"]; 121 | switch (header.toLowerCase()) { 122 | case "etag": 123 | if (!newValue) break; 124 | // fall through to default 125 | default: 126 | hasChanged = oldValue != newValue; 127 | break; 128 | } 129 | // if changed, act 130 | if (hasChanged) { 131 | Live.refreshResource(url, contentType); 132 | break; 133 | } 134 | } 135 | }); 136 | } 137 | }, 138 | 139 | // act upon a changed url of certain content type 140 | refreshResource: function (url, type) { 141 | switch (type.toLowerCase()) { 142 | // css files can be reloaded dynamically by replacing the link element 143 | case "text/css": 144 | var link = currentLinkElements[url], 145 | html = document.body.parentNode, 146 | head = link.parentNode, 147 | next = link.nextSibling, 148 | newLink = document.createElement("link"); 149 | 150 | html.className = html.className.replace(/\s*livejs\-loading/gi, '') + ' livejs-loading'; 151 | newLink.setAttribute("type", "text/css"); 152 | newLink.setAttribute("rel", "stylesheet"); 153 | newLink.setAttribute("href", url + "?now=" + new Date() * 1); 154 | next ? head.insertBefore(newLink, next) : head.appendChild(newLink); 155 | currentLinkElements[url] = newLink; 156 | oldLinkElements[url] = link; 157 | 158 | // schedule removal of the old link 159 | Live.removeoldLinkElements(); 160 | break; 161 | 162 | // check if an html resource is our current url, then reload 163 | case "text/html": 164 | if (url != document.location.href) 165 | return; 166 | 167 | // local javascript changes cause a reload as well 168 | case "text/javascript": 169 | case "application/javascript": 170 | case "application/x-javascript": 171 | document.location.reload(); 172 | } 173 | }, 174 | 175 | // removes the old stylesheet rules only once the new one has finished loading 176 | removeoldLinkElements: function () { 177 | var pending = 0; 178 | for (var url in oldLinkElements) { 179 | // if this sheet has any cssRules, delete the old link 180 | try { 181 | var link = currentLinkElements[url], 182 | oldLink = oldLinkElements[url], 183 | html = document.body.parentNode, 184 | sheet = link.sheet || link.styleSheet, 185 | rules = sheet.rules || sheet.cssRules; 186 | if (rules.length >= 0) { 187 | oldLink.parentNode.removeChild(oldLink); 188 | delete oldLinkElements[url]; 189 | setTimeout(function () { 190 | html.className = html.className.replace(/\s*livejs\-loading/gi, ''); 191 | }, 100); 192 | } 193 | } catch (e) { 194 | pending++; 195 | } 196 | if (pending) setTimeout(Live.removeoldLinkElements, 50); 197 | } 198 | }, 199 | 200 | // performs a HEAD request and passes the header info to the given callback 201 | getHead: function (url, callback) { 202 | pendingRequests[url] = true; 203 | var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XmlHttp"); 204 | xhr.open("HEAD", url, true); 205 | xhr.onreadystatechange = function () { 206 | delete pendingRequests[url]; 207 | if (xhr.readyState == 4 && xhr.status != 304) { 208 | xhr.getAllResponseHeaders(); 209 | var info = {}; 210 | for (var h in headers) { 211 | var value = xhr.getResponseHeader(h); 212 | // adjust the simple Etag variant to match on its significant part 213 | if (h.toLowerCase() == "etag" && value) value = value.replace(/^W\//, ''); 214 | if (h.toLowerCase() == "content-type" && value) value = value.replace(/^(.*?);.*?$/i, "$1"); 215 | info[h] = value; 216 | } 217 | callback(url, info); 218 | } 219 | } 220 | xhr.send(); 221 | } 222 | }; 223 | 224 | // start listening 225 | if (document.location.protocol != "file:") { 226 | if (!window.liveJsLoaded) 227 | Live.heartbeat(); 228 | 229 | window.liveJsLoaded = true; 230 | } 231 | else if (window.console) 232 | console.log("Live.js doesn't support the file protocol. It needs http."); 233 | })(); -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const Display = @import("display.zig").Display; 4 | 5 | const clock = @import("clock.zig"); 6 | const config = @import("config.zig"); 7 | 8 | const GameState = @import("gamestate.zig").GameState; 9 | const Move = @import("gamestate.zig").Move; 10 | 11 | const UiAgent = @import("ui.zig").UiAgent; 12 | const UiAgentHuman = @import("uiagenthuman.zig").UiAgentHuman; 13 | const UiAgentMachine = @import("uiagentmachine.zig").UiAgentMachine; 14 | const drawGame = @import("ui.zig").drawGame; 15 | const emitMoves = @import("ui.zig").emitMoves; 16 | const GameRecord = @import("record.zig").GameRecord; 17 | 18 | const buildopts = @import("buildopts"); 19 | 20 | pub fn main() !void { 21 | var exitReq = false; 22 | 23 | // default to human vs machine 24 | config.players[0] = try UiAgent.make("human"); 25 | config.players[1] = try UiAgent.make("machine"); 26 | 27 | config.parseCommandLine() catch { 28 | std.process.exit(1); 29 | }; 30 | 31 | clock.initTime(); 32 | 33 | // loop for terminal 34 | while (!exitReq) { 35 | var display = try Display.init(); 36 | defer display.destroy(); 37 | 38 | const sz = try Display.getSize(); 39 | if (config.mini) { 40 | if (sz.width < 80 or sz.height < 24) { 41 | std.debug.print("Display too small, must be 80x24 or larger\r\n", .{}); 42 | return; 43 | } 44 | } else { 45 | if (sz.width < 80 or sz.height < 29) { 46 | std.debug.print("Display too small, must be 80x29 or larger\r\n", .{}); 47 | return; 48 | } 49 | } 50 | 51 | display.cls(); 52 | try display.paint(); 53 | 54 | var timeout: i32 = 0; // default to not pausing, let machine agents run fast 55 | var turnN: usize = 0; 56 | var gameOver = false; 57 | var lastMoves: [config.NUM_PAWNS]Move = undefined; 58 | var gs = GameState.init(); 59 | var pi: usize = 0; // whose turn is it 60 | 61 | if (config.b64GameStart) |b64s| { 62 | // setup initial gamestate from provided b64 string 63 | const rec = try GameRecord.initFromBase64(std.heap.page_allocator, b64s); 64 | gs = try rec.toGameState(true); 65 | } 66 | 67 | try config.players[pi].selectMoveInteractive(&gs, pi); 68 | 69 | var gameRecord = try GameRecord.init(std.heap.page_allocator); 70 | defer gameRecord.deinit(); 71 | 72 | 73 | while (!gameOver) { 74 | const next = try display.getEvent(timeout); 75 | 76 | try config.players[pi].process(&gs, pi); 77 | 78 | if (try config.players[pi].handleEvent(next, &gs, pi)) { 79 | timeout = 100; // increase timeout if events being used for interaction 80 | } 81 | 82 | if (config.players[pi].getCompletedMove()) |move| { 83 | // apply the move 84 | try gs.applyMove(pi, move); 85 | try gameRecord.append(move.move); 86 | 87 | lastMoves[pi] = move.move; 88 | if (pi == config.NUM_PAWNS - 1) { // final player to take turn 89 | try emitMoves(turnN, lastMoves); 90 | turnN += 1; 91 | } 92 | 93 | if (gs.hasWon(pi)) { 94 | config.wins[pi] += 1; 95 | gameOver = true; 96 | } 97 | 98 | // select next player to make a move 99 | pi = (pi + 1) % config.NUM_PAWNS; 100 | 101 | try config.players[pi].selectMoveInteractive(&gs, pi); 102 | } 103 | 104 | switch (next) { 105 | .key => |k| switch (k) { 106 | .char => |c| switch (c) { 107 | 'q' => { 108 | exitReq = true; 109 | break; 110 | }, 111 | else => {}, 112 | }, 113 | else => {}, 114 | }, 115 | else => {}, 116 | } 117 | 118 | display.cls(); 119 | try drawGame(&display, &gs, pi); 120 | try config.players[pi].paint(&display); 121 | 122 | try display.paint(); 123 | } 124 | if (!config.playForever) { 125 | exitReq = true; 126 | // end terminal display and print game summary 127 | display.destroy(); 128 | const writer = std.io.getStdOut().writer(); 129 | const glend = try gameRecord.printGlendenningAlloc(std.heap.page_allocator); 130 | defer std.heap.page_allocator.free(glend); 131 | _ = try writer.print("{s}\n", .{glend}); 132 | const b64 = try gameRecord.toStringBase64Alloc(std.heap.page_allocator); 133 | defer std.heap.page_allocator.free(b64); 134 | _ = try writer.print("{s}\n", .{b64}); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/record.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const config = @import("config.zig"); 4 | const GameState = @import("gamestate.zig").GameState; 5 | const Dir = @import("gamestate.zig").Dir; 6 | const Move = @import("gamestate.zig").Move; 7 | const VerifiedMove = @import("gamestate.zig").VerifiedMove; 8 | const buildopts = @import("buildopts"); 9 | const native_endian = @import("builtin").target.cpu.arch.endian(); 10 | 11 | // a record of moves made in a game 12 | pub const GameRecord = struct { 13 | const Self = @This(); 14 | moves: std.ArrayList(Move) = undefined, 15 | 16 | pub fn init(alloc: std.mem.Allocator) !Self { 17 | return Self { 18 | .moves = std.ArrayList(Move).init(alloc), 19 | }; 20 | } 21 | 22 | pub fn deinit(self: *Self) void { 23 | self.moves.deinit(); 24 | } 25 | 26 | pub fn append(self: *Self, m:Move) !void { 27 | try self.moves.append(m); 28 | } 29 | 30 | pub fn getAllMoves(self: *const Self) []const Move { 31 | return self.moves.items; 32 | } 33 | 34 | pub fn toGameState(self: *const Self, verify:bool) !GameState { 35 | var gs = GameState.init(); 36 | for (self.moves.items, 0..) |m, i| { 37 | const vm = if (verify) try gs.verifyMove(i%2, m) 38 | else VerifiedMove{.legal = true, .move = m}; 39 | try gs.applyMove(i%2, vm); 40 | } 41 | return gs; 42 | } 43 | 44 | // compact binary representation, 1 byte per move 45 | // 1 bit for fence/pawn (0x80) 46 | // if pawn: 47 | // 7 bits of 9x9 nodeid (0-80) 48 | // if fence 49 | // 1 bit of v/h 50 | // 6 bits of 8x8 nodeid (0-64) 51 | 52 | const GameStatePawnExport = packed struct { 53 | cellId: u7, // 0 -> 80 54 | isPawn: u1 = 1, // msb 55 | }; 56 | 57 | const GameStateFenceExport = packed struct { 58 | isHorz: u1, // 0 = .vert, 1 = .horz 59 | cellId: u6, // 0 -> 64 60 | isPawn: u1 = 0, // msb 61 | }; 62 | 63 | pub fn toStringBase64Alloc(self: *const Self, alloc: std.mem.Allocator) ![]u8 { 64 | const b64 = std.base64.standard.Encoder; 65 | const rawbuf = try alloc.alloc(u8, self.raw_calcSize()); 66 | defer alloc.free(rawbuf); 67 | const raw = try self.encodeRaw(rawbuf); 68 | const b64buf = try alloc.alloc(u8, self.b64_calcSize()); 69 | _ = b64.encode(b64buf, raw); 70 | return b64buf; 71 | } 72 | 73 | fn b64_calcSize(self: *const Self) usize { 74 | const b64 = std.base64.standard.Encoder; 75 | return b64.calcSize(self.raw_calcSize()); 76 | } 77 | 78 | pub fn raw_calcSize(self: *const Self) usize { 79 | return self.moves.items.len; // 1 byte per move 80 | } 81 | 82 | pub fn encodeRaw(self: *const Self, buf: []u8) ![]u8 { 83 | if (buf.len < self.moves.items.len) { 84 | return error.BufTooSmallErr; 85 | } 86 | for (self.moves.items, 0..) |m, i| { 87 | switch(m) { 88 | .pawn => |pawnmove| { 89 | const cellId = @as(u8, @intCast(pawnmove.y)) * 9 + @as(u8, @intCast(pawnmove.x)); 90 | std.debug.assert(cellId < 9*9); 91 | const val = GameStatePawnExport {.isPawn = 1, .cellId = @intCast(cellId)}; 92 | buf[i] = @as(*const u8, @ptrCast(&val)).*; 93 | }, 94 | .fence => |fencemove| { 95 | const cellId = @as(u8, @intCast(fencemove.pos.y)) * 8 + @as(u8, @intCast(fencemove.pos.x)); 96 | std.debug.assert(cellId < 8*8); 97 | const val = GameStateFenceExport {.isPawn = 0, .cellId = @intCast(cellId), .isHorz = if (fencemove.dir == .horz) 1 else 0}; 98 | buf[i] = @as(*const u8, @ptrCast(&val)).*; 99 | }, 100 | } 101 | } 102 | return buf[0..self.moves.items.len]; 103 | } 104 | 105 | pub fn initFromBase64(alloc: std.mem.Allocator, b64src: []const u8) !Self { 106 | const b64 = std.base64.standard.Decoder; 107 | const rawLen = try b64.calcSizeForSlice(b64src); 108 | const rawbuf = try alloc.alloc(u8, rawLen); 109 | defer alloc.free(rawbuf); 110 | try b64.decode(rawbuf, b64src); 111 | return try initFromRaw(alloc, rawbuf); 112 | } 113 | 114 | pub fn initFromRaw(alloc: std.mem.Allocator, buf: []const u8) !Self { 115 | var self = try Self.init(alloc); 116 | errdefer self.deinit(); 117 | for (buf) |c| { 118 | if (c & 0x80 == 0x80) { // isPawn 119 | const pe:GameStatePawnExport = @as(*const GameStatePawnExport, @ptrCast(&c)).*; 120 | const y = pe.cellId / 9; 121 | const x = pe.cellId - (y*9); 122 | const move = Move{ .pawn = .{ .x = @intCast(x), .y = @intCast(y) } }; 123 | try self.append(move); 124 | } else { // fence 125 | const fe:GameStateFenceExport = @as(*const GameStateFenceExport, @ptrCast(&c)).*; 126 | const y = fe.cellId / 8; 127 | const x = fe.cellId - (y*8); 128 | const move = Move{ .fence = .{.pos = .{ .x = @intCast(x), .y = @intCast(y) }, .dir = if (fe.isHorz == 1) .horz else .vert }}; 129 | try self.append(move); 130 | } 131 | } 132 | return self; 133 | } 134 | 135 | pub fn printGlendenningAlloc(self: *const Self, alloc: std.mem.Allocator) ![]u8 { 136 | // 1. e8 a2 137 | // 2. a1v e7 138 | // ... 139 | var turn:usize = 1; 140 | var outstr:[]u8 = try std.fmt.allocPrint(alloc, "", .{}); 141 | var old:[]u8 = undefined; 142 | for (self.moves.items, 0..) |m, i| { 143 | if (i % 2 == 0) { 144 | old = outstr; 145 | outstr = try std.fmt.allocPrint(alloc, "{s}{d}.", .{old, turn}); 146 | alloc.free(old); 147 | } 148 | var mbuf:[16]u8 = undefined; 149 | const s = try m.toString(&mbuf); 150 | 151 | old = outstr; 152 | outstr = try std.fmt.allocPrint(alloc, "{s} {s}", .{old, s}); 153 | alloc.free(old); 154 | 155 | if (i % 2 == 1) { 156 | old = outstr; 157 | outstr = try std.fmt.allocPrint(alloc, "{s}\n", .{old}); 158 | alloc.free(old); 159 | turn += 1; 160 | } 161 | } 162 | return outstr; 163 | } 164 | }; 165 | 166 | 167 | -------------------------------------------------------------------------------- /src/test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const expect = std.testing.expect; 3 | 4 | const BitGraph = @import("graph.zig").BitGraph; 5 | const GameState = @import("gamestate.zig").GameState; 6 | const PosDir = @import("gamestate.zig").PosDir; 7 | const Move = @import("gamestate.zig").Move; 8 | const PosPath = @import("gamestate.zig").PosPath; 9 | const Pos = @import("gamestate.zig").Pos; 10 | const Pawn = @import("gamestate.zig").Pawn; 11 | const Dir = @import("gamestate.zig").Dir; 12 | const UiAgentMachine = @import("uiagentmachine.zig").UiAgentMachine; 13 | const config = @import("config.zig"); 14 | const UiAgent = @import("ui.zig").UiAgent; 15 | const clock = @import("clock.zig"); 16 | const GameRecord = @import("record.zig").GameRecord; 17 | 18 | test "bitgraph-edge" { 19 | var g = BitGraph.init(); 20 | try expect(!g.hasAnyEdges(11)); 21 | try expect(!g.hasAnyEdges(12)); 22 | g.addEdgeBi(11, 12); 23 | try expect(g.hasAnyEdges(11)); 24 | try expect(g.hasAnyEdges(12)); 25 | try expect(g.hasEdgeUni(11, 12)); 26 | try expect(g.hasEdgeUni(12, 11)); 27 | g.delEdgeBi(11, 12); 28 | try expect(!g.hasAnyEdges(11)); 29 | try expect(!g.hasAnyEdges(12)); 30 | try expect(!g.hasEdgeUni(11, 12)); 31 | try expect(!g.hasEdgeUni(12, 11)); 32 | } 33 | 34 | test "bitgraph-delnode" { 35 | var g = BitGraph.init(); 36 | g.addGridEdges(); 37 | 38 | try expect(g.hasEdgeUni(12, 11)); 39 | try expect(g.hasEdgeUni(12, 13)); 40 | try expect(g.hasEdgeUni(12, 3)); 41 | try expect(g.hasEdgeUni(12, 21)); 42 | 43 | g.delNode(12); 44 | 45 | try expect(g.hasEdgeUni(11, 13)); 46 | try expect(g.hasEdgeUni(11, 21)); 47 | try expect(g.hasEdgeUni(11, 3)); 48 | try expect(g.hasEdgeUni(3, 21)); 49 | try expect(g.hasEdgeUni(3, 13)); 50 | try expect(g.hasEdgeUni(3, 11)); 51 | try expect(g.hasEdgeUni(13, 3)); 52 | try expect(g.hasEdgeUni(13, 11)); 53 | try expect(g.hasEdgeUni(13, 21)); 54 | try expect(g.hasEdgeUni(21, 11)); 55 | try expect(g.hasEdgeUni(21, 3)); 56 | try expect(g.hasEdgeUni(21, 13)); 57 | 58 | try expect(true); 59 | } 60 | 61 | test "bitgraph-path-unreachable" { 62 | var g = BitGraph.init(); 63 | 64 | var pathbuf: [BitGraph.MAXPATH]BitGraph.NodeId = undefined; 65 | const range: BitGraph.NodeIdRange = .{ 66 | .min = BitGraph.coordPosToNodeId(.{ .x = 0, .y = 8 }), 67 | .max = BitGraph.coordPosToNodeId(.{ .x = 8, .y = 8 }), 68 | }; 69 | try expect(g.findShortestPath(0, range, &pathbuf, true) == null); 70 | try expect(g.findShortestPath(0, range, &pathbuf, false) == null); 71 | } 72 | 73 | test "bitgraph-path-reachable2" { 74 | var g = BitGraph.init(); 75 | 76 | g.addGridEdges(); 77 | // divide vertically 78 | g.delCoordEdgeBi(.{ .x = 2, .y = 0 }, .{ .x = 3, .y = 0 }); 79 | g.delCoordEdgeBi(.{ .x = 2, .y = 1 }, .{ .x = 3, .y = 1 }); 80 | g.delCoordEdgeBi(.{ .x = 2, .y = 2 }, .{ .x = 3, .y = 2 }); 81 | g.delCoordEdgeBi(.{ .x = 2, .y = 3 }, .{ .x = 3, .y = 3 }); 82 | g.delCoordEdgeBi(.{ .x = 2, .y = 4 }, .{ .x = 3, .y = 4 }); 83 | g.delCoordEdgeBi(.{ .x = 2, .y = 5 }, .{ .x = 3, .y = 5 }); 84 | g.delCoordEdgeBi(.{ .x = 2, .y = 6 }, .{ .x = 3, .y = 6 }); 85 | g.delCoordEdgeBi(.{ .x = 2, .y = 7 }, .{ .x = 3, .y = 7 }); 86 | g.delCoordEdgeBi(.{ .x = 2, .y = 8 }, .{ .x = 3, .y = 8 }); 87 | 88 | // on connection 89 | g.addCoordEdgeBi(.{ .x = 2, .y = 5 }, .{ .x = 3, .y = 5 }); 90 | 91 | var pathbuf: [BitGraph.MAXPATH]BitGraph.NodeId = undefined; 92 | 93 | const range: BitGraph.NodeIdRange = .{ 94 | .min = BitGraph.coordPosToNodeId(.{ .x = 8, .y = 8 }), 95 | .max = BitGraph.coordPosToNodeId(.{ .x = 8, .y = 8 }), 96 | }; 97 | 98 | try expect(g.findShortestPath(0, range, &pathbuf, true) != null); 99 | 100 | const p = g.findShortestPath(0, range, &pathbuf, true); 101 | const anyplen = p.?.len; 102 | 103 | if (g.findShortestPath(0, range, &pathbuf, false)) |path| { 104 | //const expectedPath = [_]BitGraph.NodeId{ 0, 9, 18, 27, 36, 45, 46, 47, 48, 57, 66, 75, 76, 77, 78, 79, 80 }; 105 | //try expect(std.mem.eql(BitGraph.NodeId, path, &expectedPath)); 106 | try expect(path.len <= anyplen); // check that optimal path at least as good as anypath 107 | } else { 108 | try expect(false); 109 | } 110 | 111 | // remove connection 112 | g.delCoordEdgeBi(.{ .x = 2, .y = 5 }, .{ .x = 3, .y = 5 }); 113 | 114 | try expect(g.findShortestPath(0, range, &pathbuf, true) == null); 115 | try expect(g.findShortestPath(0, range, &pathbuf, false) == null); 116 | } 117 | 118 | test "bitgraph-path-reachable" { 119 | var g = BitGraph.init(); 120 | g.addGridEdges(); 121 | 122 | var pathbuf: [BitGraph.MAXPATH]BitGraph.NodeId = undefined; 123 | const range: BitGraph.NodeIdRange = .{ 124 | .min = BitGraph.coordPosToNodeId(.{ .x = 0, .y = 8 }), 125 | .max = BitGraph.coordPosToNodeId(.{ .x = 8, .y = 8 }), 126 | }; 127 | 128 | try expect(g.findShortestPath(0, range, &pathbuf, true) != null); 129 | 130 | if (g.findShortestPath(0, range, &pathbuf, false)) |path| { 131 | const expectedPath = [_]BitGraph.NodeId{ 0, 9, 18, 27, 36, 45, 54, 63, 72 }; 132 | try expect(std.mem.eql(BitGraph.NodeId, path, &expectedPath)); 133 | } else { 134 | try expect(false); 135 | } 136 | 137 | g.delCoordEdgeBi(.{ .x = 0, .y = 0 }, .{ .x = 0, .y = 1 }); 138 | 139 | try expect(g.findShortestPath(0, range, &pathbuf, true) != null); 140 | 141 | if (g.findShortestPath(0, range, &pathbuf, false)) |path| { 142 | const expectedPath = [_]BitGraph.NodeId{ 0, 1, 10, 19, 28, 37, 46, 55, 64, 73 }; 143 | try expect(std.mem.eql(BitGraph.NodeId, path, &expectedPath)); 144 | } else { 145 | try expect(false); 146 | } 147 | } 148 | 149 | test "gamestate-fenceplace" { 150 | const f1 = PosDir{ 151 | .pos = .{ .x = 0, .y = 0 }, 152 | .dir = .horz, 153 | }; 154 | 155 | const f2 = PosDir{ 156 | .pos = .{ .x = 0, .y = 0 }, 157 | .dir = .vert, 158 | }; 159 | 160 | const f3 = PosDir{ 161 | .pos = .{ .x = 0, .y = 1 }, 162 | .dir = .horz, 163 | }; 164 | 165 | const f4 = PosDir{ 166 | .pos = .{ .x = 1, .y = 0 }, 167 | .dir = .vert, 168 | }; 169 | 170 | const f5 = PosDir{ 171 | .pos = .{ .x = 2, .y = 0 }, 172 | .dir = .horz, 173 | }; 174 | 175 | const f6 = PosDir{ 176 | .pos = .{ .x = 0, .y = 2 }, 177 | .dir = .vert, 178 | }; 179 | 180 | const f7 = PosDir{ 181 | .pos = .{ .x = 0, .y = 1 }, 182 | .dir = .vert, 183 | }; 184 | 185 | const f8 = PosDir{ 186 | .pos = .{ .x = 1, .y = 1 }, 187 | .dir = .vert, 188 | }; 189 | 190 | const f9 = PosDir{ 191 | .pos = .{ .x = 1, .y = 0 }, 192 | .dir = .horz, 193 | }; 194 | 195 | var gs = GameState.init(); 196 | // can place either 197 | try expect(gs.canPlaceFence(f1)); 198 | try expect(gs.canPlaceFence(f2)); 199 | 200 | // can't place crossing overlap v over h 201 | gs.placeFence(0, f1); 202 | try expect(!gs.canPlaceFence(f2)); 203 | 204 | // can't place crossing overlap h over v 205 | gs = GameState.init(); 206 | gs.placeFence(0, f2); 207 | try expect(!gs.canPlaceFence(f1)); 208 | 209 | // can place parallel h 210 | gs = GameState.init(); 211 | gs.placeFence(0, f1); 212 | try expect(gs.canPlaceFence(f3)); 213 | gs.placeFence(0, f3); 214 | 215 | // can place parallel v 216 | gs = GameState.init(); 217 | gs.placeFence(0, f2); 218 | try expect(gs.canPlaceFence(f4)); 219 | gs.placeFence(0, f4); 220 | 221 | // can place end to end h 222 | gs = GameState.init(); 223 | gs.placeFence(0, f1); 224 | try expect(gs.canPlaceFence(f5)); 225 | gs.placeFence(0, f5); 226 | 227 | // can place end to end v 228 | gs = GameState.init(); 229 | gs.placeFence(0, f2); 230 | try expect(gs.canPlaceFence(f6)); 231 | gs.placeFence(0, f6); 232 | 233 | // cannot place overlap h 234 | gs = GameState.init(); 235 | gs.placeFence(0, f1); 236 | try expect(!gs.canPlaceFence(f9)); 237 | 238 | // cannot place overlap v 239 | gs = GameState.init(); 240 | gs.placeFence(0, f2); 241 | try expect(!gs.canPlaceFence(f7)); 242 | 243 | // can place t shape 244 | gs = GameState.init(); 245 | gs.placeFence(0, f1); 246 | try expect(gs.canPlaceFence(f8)); 247 | gs.placeFence(0, f8); 248 | } 249 | 250 | test "gamestate-fenceplace-T" { 251 | var gs = GameState.init(); 252 | 253 | const f1 = PosDir{ 254 | .pos = .{ .x = 0, .y = 1 }, 255 | .dir = .vert, 256 | }; 257 | 258 | const f2 = PosDir{ 259 | .pos = .{ .x = 0, .y = 0 }, 260 | .dir = .horz, 261 | }; 262 | 263 | try expect(gs.canPlaceFence(f1)); 264 | gs.placeFence(0, f1); 265 | try expect(gs.canPlaceFence(f2)); 266 | gs.placeFence(0, f2); 267 | } 268 | 269 | test "gamestate-fenceplace-blockpawn" { 270 | var gs = GameState.init(); 271 | var x: usize = 0; 272 | while (x < 8) : (x += 2) { 273 | gs.placeFence(0, .{ 274 | .pos = .{ .x = @intCast(x), .y = @intCast(3) }, 275 | .dir = .horz, 276 | }); 277 | } 278 | 279 | const f1 = PosDir{ 280 | .pos = .{ .x = 7, .y = 2 }, 281 | .dir = .vert, 282 | }; 283 | 284 | const f2 = PosDir{ 285 | .pos = .{ .x = 7, .y = 1 }, 286 | .dir = .horz, 287 | }; 288 | 289 | try expect(gs.canPlaceFence(f1)); 290 | gs.placeFence(0, f1); 291 | try expect(!gs.canPlaceFence(f2)); 292 | } 293 | 294 | test "gamestate-pawnmove" { 295 | var gs = GameState.init(); 296 | 297 | // can move down, up, right, left 298 | var pos = gs.getPawnPos(0); 299 | pos.y += 1; 300 | try expect(gs.canMovePawn(0, pos)); 301 | gs.movePawn(0, pos); 302 | pos.y -= 1; 303 | try expect(gs.canMovePawn(0, pos)); 304 | gs.movePawn(0, pos); 305 | pos.x += 1; 306 | try expect(gs.canMovePawn(0, pos)); 307 | gs.movePawn(0, pos); 308 | pos.x -= 1; 309 | try expect(gs.canMovePawn(0, pos)); 310 | gs.movePawn(0, pos); 311 | 312 | // X| 313 | // | 314 | const f1 = PosDir{ 315 | .pos = .{ .x = 4, .y = 0 }, 316 | .dir = .vert, 317 | }; 318 | gs = GameState.init(); 319 | pos = gs.getPawnPos(0); 320 | gs.placeFence(0, f1); 321 | pos.x += 1; 322 | try expect(!gs.canMovePawn(0, pos)); 323 | 324 | // |X 325 | // | 326 | const f2 = PosDir{ 327 | .pos = .{ .x = 3, .y = 0 }, 328 | .dir = .vert, 329 | }; 330 | gs = GameState.init(); 331 | pos = gs.getPawnPos(0); 332 | gs.placeFence(0, f2); 333 | pos.x -= 1; 334 | try expect(!gs.canMovePawn(0, pos)); 335 | 336 | // X 337 | // -- 338 | const f3 = PosDir{ 339 | .pos = .{ .x = 3, .y = 0 }, 340 | .dir = .horz, 341 | }; 342 | gs = GameState.init(); 343 | pos = gs.getPawnPos(0); 344 | gs.placeFence(0, f3); 345 | pos.y += 1; 346 | try expect(!gs.canMovePawn(0, pos)); 347 | 348 | // -- 349 | // X 350 | const f4 = PosDir{ 351 | .pos = .{ .x = 3, .y = 7 }, 352 | .dir = .horz, 353 | }; 354 | gs = GameState.init(); 355 | pos = gs.getPawnPos(1); 356 | gs.placeFence(0, f4); 357 | pos.y -= 1; 358 | try expect(!gs.canMovePawn(0, pos)); 359 | } 360 | 361 | test "gamestate-jumponwin" { 362 | // pawn should be able to end game by jumping onto opponent if they're blocking the goal line 363 | var gs = GameState.init(); 364 | 365 | const f1 = PosDir{ 366 | .pos = .{ .x = 0, .y = 7 }, 367 | .dir = .vert, 368 | }; 369 | gs = GameState.init(); 370 | gs.placeFence(0, f1); 371 | 372 | gs.pawns[0].pos.x = 0; 373 | gs.pawns[0].pos.y = 7; 374 | 375 | gs.pawns[1].pos.x = 0; 376 | gs.pawns[1].pos.y = 8; 377 | 378 | try expect(gs.canMovePawn(0, .{.x=0, .y=8})); 379 | } 380 | 381 | test "coordpos" { 382 | // convert node id into coords and back 383 | var i: BitGraph.NodeId = 0; 384 | for (0..9) |y| { 385 | for (0..9) |x| { 386 | const cp = BitGraph.nodeIdToCoordPos(i); 387 | const ni = BitGraph.coordPosToNodeId(.{ .x = @intCast(x), .y = @intCast(y) }); 388 | try expect(ni == i); 389 | try expect(cp.x == x and cp.y == y); 390 | i += 1; 391 | } 392 | } 393 | } 394 | 395 | test "gamestate-pawnpath" { 396 | var gs = GameState.init(); 397 | var x: usize = 0; 398 | // place obstacles 399 | while (x < 8) : (x += 2) { 400 | gs.placeFence(0, .{ 401 | .pos = .{ .x = @intCast(x), .y = @intCast(3) }, 402 | .dir = .horz, 403 | }); 404 | } 405 | 406 | const pos = gs.getPawnPos(0); 407 | var pathbuf: [BitGraph.MAXPATH]BitGraph.NodeId = undefined; 408 | 409 | const goal = gs.getPawnGoal(0); 410 | 411 | // plan path 412 | if (gs.graph.findShortestPath(BitGraph.coordPosToNodeId(.{ .x = @intCast(pos.x), .y = @intCast(pos.y) }), goal, &pathbuf, false)) |path| { 413 | // follow path, skip starting pos at start of list 414 | for (path[1..path.len]) |n| { 415 | const nextpos = BitGraph.nodeIdToCoordPos(n); 416 | try expect(gs.canMovePawn(0, nextpos)); 417 | gs.movePawn(0, nextpos); 418 | } 419 | } else { 420 | try expect(false); 421 | } 422 | } 423 | 424 | test "gamestate-pawnonpawn" { 425 | var gs = GameState.init(); 426 | 427 | // Place pawn 1 to right of pawn 0 428 | 429 | gs.pawns[0].pos.x = 4; 430 | gs.pawns[0].pos.y = 4; 431 | 432 | gs.pawns[1].pos.x = 5; 433 | gs.pawns[1].pos.y = 4; 434 | 435 | var pos = gs.getPawnPos(0); 436 | pos.x += 1; 437 | try expect(!gs.canMovePawn(0, pos)); // cannot land on pawn 438 | 439 | pos = gs.getPawnPos(0); 440 | pos.x += 2; 441 | try expect(gs.canMovePawn(0, pos)); // can jump over pawn 442 | } 443 | 444 | test "gamestate-pawnonpawnwall" { 445 | var gs = GameState.init(); 446 | 447 | gs.pawns[0].pos.x = 4; 448 | gs.pawns[0].pos.y = 5; 449 | 450 | gs.pawns[1].pos.x = 4; 451 | gs.pawns[1].pos.y = 6; 452 | 453 | // -- 454 | // 0 455 | // 1 456 | const f1 = PosDir{ 457 | .pos = .{ .x = 4, .y = 4 }, 458 | .dir = .horz, 459 | }; 460 | gs.placeFence(0, f1); 461 | var pos = gs.getPawnPos(1); 462 | pos.y -= 1; 463 | pos.x += 1; 464 | try expect(gs.canMovePawn(1, pos)); 465 | 466 | pos = gs.getPawnPos(1); 467 | pos.y -= 1; 468 | pos.x -= 1; 469 | try expect(gs.canMovePawn(1, pos)); 470 | 471 | pos = gs.getPawnPos(1); 472 | pos.y -= 2; 473 | try expect(!gs.canMovePawn(1, pos)); 474 | 475 | pos = gs.getPawnPos(1); 476 | pos.y -= 2; 477 | pos.x += 1; 478 | try expect(!gs.canMovePawn(1, pos)); 479 | 480 | pos = gs.getPawnPos(1); 481 | pos.y -= 2; 482 | pos.x -= 1; 483 | try expect(!gs.canMovePawn(1, pos)); 484 | } 485 | 486 | test "gamestate-findpath" { 487 | var gs = GameState.init(); 488 | var x: usize = 0; 489 | // place obstacles 490 | while (x < 8) : (x += 2) { 491 | gs.placeFence(0, .{ 492 | .pos = .{ .x = @intCast(x), .y = @intCast(3) }, 493 | .dir = .horz, 494 | }); 495 | } 496 | 497 | var pathbuf: PosPath = undefined; 498 | const pathO = gs.findShortestPath(0, gs.getPawnPos(0), &pathbuf); 499 | 500 | if (pathO) |path| { 501 | const expectedPath = [_]Pos { .{ .x = 5, .y = 0 }, .{ .x = 6, .y = 0 }, .{ .x = 6, .y = 1 }, .{ .x = 7, .y = 1 }, .{ .x = 7, .y = 2 }, .{ .x = 7, .y = 3 }, .{ .x = 8, .y = 3 }, .{ .x = 8, .y = 4 }, .{ .x = 8, .y = 5 }, .{ .x = 8, .y = 6 }, .{ .x = 8, .y = 7 }, .{ .x = 8, .y = 8 } }; 502 | 503 | try expect(path.len == expectedPath.len); 504 | for (path, 0..) |p, i| { 505 | try expect(p.x == expectedPath[i].x and p.y == expectedPath[i].y); 506 | } 507 | } else { 508 | try expect(false); 509 | } 510 | } 511 | 512 | test "gamestate-findpath-pawnjump" { 513 | var gs = GameState.init(); 514 | // place obstacles 515 | // | 516 | // |0 517 | // |1 518 | // | 519 | var y: usize = 0; 520 | while (y < 8) : (y += 2) { 521 | gs.placeFence(0, .{ 522 | .pos = .{ .x = @intCast(3), .y = @intCast(y) }, 523 | .dir = .vert, 524 | }); 525 | } 526 | 527 | gs.pawns[1].pos.y -= 1; // move off of the goal line for simplicity 528 | 529 | var pathbuf: PosPath = undefined; 530 | const pathO = gs.findShortestPath(0, gs.getPawnPos(0), &pathbuf); 531 | if (pathO) |path| { 532 | // checking it jumps over the final pawn 533 | const expectedPath = [_]Pos{ .{ .x = 4, .y = 1 }, .{ .x = 4, .y = 2 }, .{ .x = 4, .y = 3 }, .{ .x = 4, .y = 4 }, .{ .x = 4, .y = 5 }, .{ .x = 4, .y = 6 }, .{ .x = 4, .y = 8 } }; 534 | try expect(path.len == expectedPath.len); 535 | for (path, 0..) |p, i| { 536 | try expect(p.x == expectedPath[i].x and p.y == expectedPath[i].y); 537 | } 538 | } else { 539 | try expect(false); 540 | } 541 | } 542 | 543 | test "finderr1" { 544 | var gs = GameState.init(); 545 | 546 | const pi = 0; 547 | gs.graph = .{ .bitMatrix = .{ 514, 1029, 10, 20, 40, 80, 160, 320, 131200, 1025, 2562, 5120, 10240, 20480, 8192, 16842752, 33718272, 67174656, 134742016, 269746176, 539492352, 1078984704, 2157969408, 4299161600, 8623521792, 17263820800, 34393423872, 68988174336, 138110566400, 276221132800, 552442265600, 1104884531200, 2209769062400, 4419538124800, 8839076249600, 17609433022464, 35321945260032, 343865819136, 687731638272, 1375463276544, 2750926553088, 1131401759948800, 2262803519897600, 4525607039795200, 9016029707501568, 70437463654400, 175921860444160, 72409437758816256, 144818875517632512, 288511851128422400, 2253998836940800, 5633897580724224, 11267795161448448, 4521191813414912, 9259400833873739776, 90071992547409920, 180284722583175168, 360569445166350336, 144678138029277184, 295147905179352825856, 592601653367919345664, 1186356228240445538304, 2363489084444036300800, 4740831241341864247296, 9490849825923564306432, 92233720368547758080, 184467440737095516160, 368934881474191032320, 148150413341979836416, 303413199445879311826944, 607416694702117329305600, 1210111022921365013397504, 9453956337776145203200, 23630279158421935620096, 47223664828696452136960, 94447329657392904273920, 188894659314785808547840, 377789318629571617095680, 756168933069501939843072, 1512337866139003879686144, 606824093048749409959936 } }; 548 | gs.pawns = .{ Pawn{ .pos = .{ .x = 2, .y = 0 }, .goaly = 8, .numFencesRemaining = 1 }, Pawn{ .pos = .{ .x = 1, .y = 7 }, .goaly = 0, .numFencesRemaining = 1 } }; 549 | gs.fences = .{ PosDir{ .pos = .{ .x = 3, .y = 4 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 5, .y = 5 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 3, .y = 6 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 2, .y = 7 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 1, .y = 4 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 7, .y = 5 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 4, .y = 7 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 5, .y = 6 }, .dir = Dir.vert }, PosDir{ .pos = .{ .x = 0, .y = 1 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 2, .y = 1 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 4, .y = 1 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 4, .y = 5 }, .dir = Dir.vert }, PosDir{ .pos = .{ .x = 5, .y = 1 }, .dir = Dir.vert }, PosDir{ .pos = .{ .x = 4, .y = 0 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 2, .y = 0 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 6, .y = 0 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 0, .y = 5 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 1, .y = 6 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 0, .y = 0 }, .dir = Dir.vert }, PosDir{ .pos = .{ .x = 0, .y = 0 }, .dir = Dir.vert } }; 550 | 551 | var pathbuf: PosPath = undefined; 552 | if (gs.findShortestPath(pi, gs.getPawnPos(pi), &pathbuf)) |_| { 553 | //std.debug.print("path={any}\r\n", .{path}); 554 | } else { 555 | try expect(false); 556 | } 557 | } 558 | 559 | test "gamerr1" { 560 | var gs = GameState.init(); 561 | 562 | const pi = 0; 563 | gs.graph = .{ .bitMatrix = .{ 514, 1029, 10, 20, 40, 80, 160, 320, 131200, 1025, 2562, 5120, 10240, 20480, 8192, 16842752, 33718272, 67174656, 134742016, 269746176, 539492352, 1078984704, 2157969408, 4299161600, 8623521792, 17263820800, 34393423872, 68988174336, 138110566400, 276221132800, 552442265600, 1104884531200, 2209769062400, 4419538124800, 8839076249600, 17609433022464, 35321945260032, 343865819136, 687731638272, 1375463276544, 2750926553088, 1131401759948800, 2262803519897600, 4525607039795200, 9016029707501568, 70437463654400, 175921860444160, 72409437758816256, 144818875517632512, 288511851128422400, 2253998836940800, 5633897580724224, 11267795161448448, 4521191813414912, 9259400833873739776, 90071992547409920, 180284722583175168, 360569445166350336, 144678138029277184, 295147905179352825856, 592601653367919345664, 1186356228240445538304, 2363489084444036300800, 4740831241341864247296, 9490849825923564306432, 92233720368547758080, 184467440737095516160, 368934881474191032320, 148150413341979836416, 303413199445879311826944, 607416694702117329305600, 1210111022921365013397504, 9453956337776145203200, 23630279158421935620096, 47223664828696452136960, 94447329657392904273920, 188894659314785808547840, 377789318629571617095680, 756168933069501939843072, 1512337866139003879686144, 606824093048749409959936 } }; 564 | gs.pawns = .{ Pawn{ .pos = .{ .x = 2, .y = 0 }, .goaly = 8, .numFencesRemaining = 1 }, Pawn{ .pos = .{ .x = 1, .y = 7 }, .goaly = 0, .numFencesRemaining = 1 } }; 565 | gs.fences = .{ PosDir{ .pos = .{ .x = 3, .y = 4 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 5, .y = 5 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 3, .y = 6 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 2, .y = 7 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 1, .y = 4 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 7, .y = 5 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 4, .y = 7 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 5, .y = 6 }, .dir = Dir.vert }, PosDir{ .pos = .{ .x = 0, .y = 1 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 2, .y = 1 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 4, .y = 1 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 4, .y = 5 }, .dir = Dir.vert }, PosDir{ .pos = .{ .x = 5, .y = 1 }, .dir = Dir.vert }, PosDir{ .pos = .{ .x = 4, .y = 0 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 2, .y = 0 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 6, .y = 0 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 0, .y = 5 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 1, .y = 6 }, .dir = Dir.horz }, PosDir{ .pos = .{ .x = 0, .y = 0 }, .dir = Dir.vert }, PosDir{ .pos = .{ .x = 0, .y = 0 }, .dir = Dir.vert } }; 566 | 567 | //gs.print(); 568 | 569 | var machine = UiAgentMachine.init(); 570 | try machine.selectMoveInteractive(&gs, pi); 571 | _ = try machine.process(&gs, pi); 572 | 573 | if (machine.getCompletedMove()) |_| { 574 | //std.debug.print("{any}\r\n", .{vm}); 575 | try expect(true); 576 | } else { 577 | try expect(false); 578 | } 579 | } 580 | 581 | test "record" { 582 | var record = try GameRecord.init(std.heap.page_allocator); 583 | defer record.deinit(); 584 | 585 | const moves:[4]Move = .{ 586 | .{ .pawn = .{ .x = 4, .y = 1 } }, 587 | .{ .pawn = .{ .x = 4, .y = 7 } }, 588 | .{ .fence = .{.pos = .{ .x = 7, .y = 7 }, .dir = .horz }}, 589 | .{ .fence = .{.pos = .{ .x = 0, .y = 1 }, .dir = .vert }}, 590 | }; 591 | const expectedRaw:[4]u8 = .{ 141, 195, 127, 16 }; 592 | 593 | // record all moves 594 | for (moves) |m| { 595 | try record.append(m); 596 | } 597 | 598 | // check stored list is same 599 | var storedMoves = record.getAllMoves(); 600 | try expect(storedMoves.len == moves.len); 601 | for (0..moves.len) |i| { 602 | try expect(std.meta.eql(storedMoves[i], moves[i])); 603 | } 604 | 605 | // get raw byte representation 606 | var rawbuf:[128]u8 = undefined; 607 | const raw = try record.encodeRaw(&rawbuf); 608 | try expect(raw.len == record.raw_calcSize()); 609 | try expect(std.mem.eql(u8, &expectedRaw, raw)); 610 | 611 | // convert raw back to new record 612 | var rec2 = try GameRecord.initFromRaw(std.heap.page_allocator, raw); 613 | defer rec2.deinit(); 614 | storedMoves = rec2.getAllMoves(); 615 | try expect(storedMoves.len == moves.len); 616 | for (0..moves.len) |i| { 617 | try expect(std.meta.eql(storedMoves[i], moves[i])); 618 | } 619 | 620 | // check Glendenning representation 621 | const s = try rec2.printGlendenningAlloc(std.heap.page_allocator); 622 | defer std.heap.page_allocator.free(s); 623 | try expect(std.mem.eql(u8, s, "1. e2 e8\n2. h8h a2v\n")); 624 | 625 | // encode to base64 626 | const sb64 = try record.toStringBase64Alloc(std.heap.page_allocator); 627 | // decode base64 and check 628 | var rec3 = try GameRecord.initFromBase64(std.heap.page_allocator, sb64); 629 | defer rec3.deinit(); 630 | storedMoves = rec3.getAllMoves(); 631 | try expect(storedMoves.len == moves.len); 632 | for (0..moves.len) |i| { 633 | try expect(std.meta.eql(storedMoves[i], moves[i])); 634 | } 635 | } 636 | 637 | //test "speed" { 638 | // var pi:usize = 0; 639 | // config.players[0] = try UiAgent.make("random"); 640 | // config.players[1] = try UiAgent.make("random"); 641 | // const runs = 100; 642 | // 643 | // clock.initTime(); 644 | // const start = clock.millis(); 645 | // 646 | // for (0..runs) |_| { 647 | // var gs = GameState.init(); 648 | // while(!gs.hasWon(0) and !gs.hasWon(1)) { 649 | // try config.players[pi].selectMoveInteractive(&gs, pi); 650 | // try config.players[pi].process(&gs, pi); 651 | // // FIXME assumes move is available immediately, should poll for it and call process repeatedly 652 | // if (config.players[pi].getCompletedMove()) |vmove| { 653 | // try gs.applyMove(pi, vmove); 654 | // pi = (pi + 1) % config.NUM_PAWNS; 655 | // } 656 | // } 657 | // } 658 | // const end = clock.millis(); 659 | // std.debug.print("t={any}\r\n", .{end-start}); 660 | // 661 | // try expect(end-start < 3000); 662 | // 663 | // 664 | //} 665 | -------------------------------------------------------------------------------- /src/ui.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const Display = @import("display.zig").Display; 4 | 5 | const io = std.io; 6 | 7 | const mibu = @import("mibu"); 8 | const events = mibu.events; 9 | const term = mibu.term; 10 | const color = mibu.color; 11 | 12 | const clock = @import("clock.zig"); 13 | const config = @import("config.zig"); 14 | 15 | const GameState = @import("gamestate.zig").GameState; 16 | const PosDir = @import("gamestate.zig").PosDir; 17 | const PosPath = @import("gamestate.zig").PosPath; 18 | const Pos = @import("gamestate.zig").Pos; 19 | const Dir = @import("gamestate.zig").Dir; 20 | const Move = @import("gamestate.zig").Move; 21 | const VerifiedMove = @import("gamestate.zig").VerifiedMove; 22 | 23 | const UiAgentMachine = @import("uiagentmachine.zig").UiAgentMachine; 24 | const UiAgentHuman = @import("uiagenthuman.zig").UiAgentHuman; 25 | const UiAgentRandom = @import("uiagentrandom.zig").UiAgentRandom; 26 | 27 | 28 | // Interface for playing agents 29 | pub const UiAgent = union(enum) { 30 | human: UiAgentHuman, 31 | machine: UiAgentMachine, 32 | random: UiAgentRandom, 33 | 34 | pub fn getName(self: UiAgent, buf:[]u8) ![]const u8 { 35 | return try std.fmt.bufPrint(buf, "{s}", .{@tagName(self)}); 36 | } 37 | 38 | pub fn make(name:[]const u8) !UiAgent { 39 | if (std.mem.eql(u8, name, "human")) { 40 | return UiAgent{.human = UiAgentHuman.init()}; 41 | } 42 | if (std.mem.eql(u8, name, "machine")) { 43 | return UiAgent{.machine = UiAgentMachine.init()}; 44 | } 45 | if (std.mem.eql(u8, name, "random")) { 46 | return UiAgent{.random = UiAgentRandom.init()}; 47 | } 48 | return error.InvalidAgentErr; 49 | } 50 | 51 | // start searching for a move to make 52 | pub fn selectMoveInteractive(self: *UiAgent, gs: *const GameState, pi: usize) !void { 53 | switch(self.*) { 54 | inline else => |*case| return case.selectMoveInteractive(gs, pi), 55 | } 56 | } 57 | 58 | // handle any UI events 59 | pub fn process(self: *UiAgent, gs: *const GameState, pi: usize) !void { 60 | switch(self.*) { 61 | inline else => |*case| return case.process(gs, pi), 62 | } 63 | } 64 | 65 | // handle any UI events 66 | pub fn handleEvent(self: *UiAgent, event: events.Event, gs: *const GameState, pi: usize) !bool { 67 | switch(self.*) { 68 | inline else => |*case| return case.handleEvent(event, gs, pi), 69 | } 70 | } 71 | 72 | // paint anything to display 73 | pub fn paint(self: *UiAgent, display: *Display) !void { 74 | switch(self.*) { 75 | inline else => |*case| return case.paint(display), 76 | } 77 | } 78 | 79 | // return chosen move, if one has been found. Will be polled 80 | pub fn getCompletedMove(self: *UiAgent) ?VerifiedMove { 81 | switch(self.*) { 82 | inline else => |*case| return case.getCompletedMove(), 83 | } 84 | } 85 | }; 86 | 87 | pub fn drawGame(display: *Display, gs: *GameState, gspi: usize) !void { 88 | try drawStats(display, gs, gspi); 89 | drawBoard(display); 90 | for (gs.pawns, 0..) |p, pi| { 91 | drawPawn(display, p.pos.x, p.pos.y, config.pawnColour[pi]); 92 | } 93 | for (gs.getFences()) |f| { 94 | drawFence(display, f.pos.x, f.pos.y, config.fenceColour, f.dir); 95 | } 96 | } 97 | 98 | fn paintString(display: *Display, bg: color.Color, fg: color.Color, bold: bool, xpos: usize, ypos: usize, sl: []u8) !void { 99 | var strx = xpos; 100 | for (sl) |elem| { 101 | try display.setPixel(strx, ypos, .{ .fg = fg, .bg = bg, .c = elem, .bold = bold }); 102 | strx += 1; 103 | } 104 | } 105 | 106 | fn drawStats(display: *Display, gs: *const GameState, pi: usize) !void { 107 | var buf: [128]u8 = undefined; 108 | 109 | var statsXoff: usize = 0; 110 | var statsYoff: usize = 0; 111 | 112 | if (config.mini) { 113 | statsXoff = 41; 114 | statsYoff = 3; 115 | } else { 116 | statsXoff = 59; 117 | statsYoff = 2; 118 | } 119 | 120 | var name0Buf:[64]u8 = undefined; 121 | var name1Buf:[64]u8 = undefined; 122 | const name0 = try config.players[0].getName(&name0Buf); 123 | const name1 = try config.players[1].getName(&name1Buf); 124 | 125 | try paintString(display, .black, .white, pi == 0, statsXoff, statsYoff, try std.fmt.bufPrint(&buf, "Player 1: {s}", .{name0})); 126 | try paintString(display, .black, .white, pi == 0, statsXoff, statsYoff + 1, try std.fmt.bufPrint(&buf, "Wins: {d}", .{config.wins[0]})); 127 | try paintString(display, .black, .white, pi == 0, statsXoff, statsYoff + 2, try std.fmt.bufPrint(&buf, "Fences: {d}", .{gs.pawns[0].numFencesRemaining})); 128 | 129 | try paintString(display, .black, .white, pi == 1, statsXoff, statsYoff + 4, try std.fmt.bufPrint(&buf, "Player 2: {s}", .{name1})); 130 | try paintString(display, .black, .white, pi == 1, statsXoff, statsYoff + 5, try std.fmt.bufPrint(&buf, "Wins: {d}", .{config.wins[1]})); 131 | try paintString(display, .black, .white, pi == 1, statsXoff, statsYoff + 6, try std.fmt.bufPrint(&buf, "Fences: {d}", .{gs.pawns[1].numFencesRemaining})); 132 | 133 | if (gs.hasWon(0)) { 134 | try paintString(display, .black, .white, true, statsXoff, statsYoff + 15, try std.fmt.bufPrint(&buf, "Player1 won", .{})); 135 | try paintString(display, .black, .white, true, statsXoff, statsYoff + 16, try std.fmt.bufPrint(&buf, "Player2 lost", .{})); 136 | } 137 | if (gs.hasWon(1)) { 138 | try paintString(display, .black, .white, true, statsXoff, statsYoff + 15, try std.fmt.bufPrint(&buf, "Player1 lost", .{})); 139 | try paintString(display, .black, .white, true, statsXoff, statsYoff + 16, try std.fmt.bufPrint(&buf, "Player2 won", .{})); 140 | } 141 | 142 | try paintString(display, .black, .white, true, statsXoff, statsYoff + 8, try std.fmt.bufPrint(&buf, "q - quit", .{})); 143 | try paintString(display, .black, .white, true, statsXoff, statsYoff + 9, try std.fmt.bufPrint(&buf, "cursors - set pos", .{})); 144 | try paintString(display, .black, .white, true, statsXoff, statsYoff + 10, try std.fmt.bufPrint(&buf, "enter - confirm", .{})); 145 | try paintString(display, .black, .white, true, statsXoff, statsYoff + 11, try std.fmt.bufPrint(&buf, "tab - fence/pawn", .{})); 146 | try paintString(display, .black, .white, true, statsXoff, statsYoff + 12, try std.fmt.bufPrint(&buf, "space - rotate fence", .{})); 147 | 148 | if (config.lastTurnStr) |s| { 149 | try paintString(display, .black, .white, true, statsXoff, statsYoff + 14, try std.fmt.bufPrint(&buf, "{s}", .{s})); 150 | } 151 | } 152 | 153 | fn drawBoard(display: *Display) void { 154 | if (config.mini) { 155 | // draw squares 156 | for (0..config.GRIDSIZE) |x| { 157 | for (0..config.GRIDSIZE) |y| { 158 | if (x == 0) { 159 | // row labels 160 | try display.setPixel(config.UI_XOFF + 4 * x, config.UI_YOFF + 2 * y, .{ .fg = .white, .bg = .blue, .c = config.ROW_LABEL_START + @as(u8, @intCast(y)), .bold = true }); 161 | } 162 | 163 | // pawn squares 164 | try display.setPixel(config.UI_XOFF + 4 * x + config.label_extra_w, config.UI_YOFF + 2 * y, .{ .fg = .white, .bg = .black, .c = ' ', .bold = false }); 165 | try display.setPixel(config.UI_XOFF + 4 * x + 1 + config.label_extra_w, config.UI_YOFF + 2 * y, .{ .fg = .white, .bg = .black, .c = ' ', .bold = false }); 166 | 167 | if (true) { 168 | if (x != config.GRIDSIZE - 1) { 169 | try display.setPixel(config.UI_XOFF + 4 * x + 2 + config.label_extra_w, config.UI_YOFF + 2 * y, .{ .fg = .white, .bg = .black, .c = ' ', .bold = false }); 170 | try display.setPixel(config.UI_XOFF + 4 * x + 3 + config.label_extra_w, config.UI_YOFF + 2 * y, .{ .fg = .white, .bg = .black, .c = ' ', .bold = false }); 171 | } 172 | 173 | if (y != config.GRIDSIZE - 1) { 174 | try display.setPixel(config.UI_XOFF + 4 * x + config.label_extra_w, config.UI_YOFF + 2 * y + 1, .{ .fg = .white, .bg = .black, .c = ' ', .bold = false }); 175 | try display.setPixel(config.UI_XOFF + 4 * x + 1 + config.label_extra_w, config.UI_YOFF + 2 * y + 1, .{ .fg = .white, .bg = .black, .c = ' ', .bold = false }); 176 | } 177 | } 178 | } 179 | } 180 | 181 | // draw fence join spots 182 | for (0..config.GRIDSIZE - 1) |xg| { 183 | for (0..config.GRIDSIZE - 1) |yg| { 184 | try display.setPixel(config.UI_XOFF + 4 * xg + 2 + config.label_extra_w, config.UI_YOFF + 2 * yg + 1, .{ .fg = .white, .bg = .white, .c = ' ', .bold = false }); 185 | try display.setPixel(config.UI_XOFF + 4 * xg + 3 + config.label_extra_w, config.UI_YOFF + 2 * yg + 1, .{ .fg = .white, .bg = .white, .c = ' ', .bold = false }); 186 | } 187 | } 188 | 189 | // column labels 190 | for (0..config.GRIDSIZE) |x| { 191 | try display.setPixel(config.UI_XOFF + 4 * x + config.label_extra_w, config.UI_YOFF + 2 * config.GRIDSIZE, .{ .fg = .white, .bg = .blue, .c = config.COLUMN_LABEL_START + @as(u8, @intCast(x)), .bold = true }); 192 | } 193 | } else { 194 | // draw border 195 | for (0..config.GRIDSIZE * 6 + 2) |x| { 196 | try display.setPixel(config.UI_XOFF + x - 2, config.UI_YOFF - 1, .{ .fg = .white, .bg = .white, .c = ' ', .bold = false }); // top 197 | try display.setPixel(config.UI_XOFF + x - 2, (config.UI_YOFF + 3 * config.GRIDSIZE) - 1, .{ .fg = .white, .bg = .white, .c = ' ', .bold = false }); // bottom 198 | } 199 | for (0..config.GRIDSIZE * 3) |y| { 200 | try display.setPixel(config.UI_XOFF - 2, config.UI_YOFF + y, .{ .fg = .white, .bg = .white, .c = ' ', .bold = false }); // left 201 | try display.setPixel(config.UI_XOFF - 1, config.UI_YOFF + y, .{ .fg = .white, .bg = .white, .c = ' ', .bold = false }); // left 202 | 203 | try display.setPixel(config.UI_XOFF + 6 * config.GRIDSIZE - 2, config.UI_YOFF + y, .{ .fg = .white, .bg = .white, .c = ' ', .bold = false }); // right 204 | try display.setPixel(config.UI_XOFF + 6 * config.GRIDSIZE - 1, config.UI_YOFF + y, .{ .fg = .white, .bg = .white, .c = ' ', .bold = false }); // right 205 | } 206 | 207 | // column labels 208 | for (0..config.GRIDSIZE) |x| { 209 | try display.setPixel(config.UI_XOFF + 6 * x + 1, config.UI_YOFF + 3 * config.GRIDSIZE - 1, .{ .fg = .black, .bg = .white, .c = config.COLUMN_LABEL_START + @as(u8, @intCast(x)), .bold = true }); 210 | } 211 | 212 | // draw squares 213 | for (0..config.GRIDSIZE) |x| { 214 | for (0..config.GRIDSIZE) |y| { 215 | if (x == 0) { 216 | // row labels 217 | try display.setPixel(config.UI_XOFF + 6 * x - 2, config.UI_YOFF + 3 * y, .{ .fg = .black, .bg = .white, .c = config.ROW_LABEL_START + @as(u8, @intCast(y)), .bold = true }); 218 | } 219 | 220 | // pawn squares 221 | try display.setPixel(config.UI_XOFF + 6 * x + 0, config.UI_YOFF + 3 * y, .{ .fg = .white, .bg = .black, .c = ' ', .bold = false }); 222 | try display.setPixel(config.UI_XOFF + 6 * x + 1, config.UI_YOFF + 3 * y, .{ .fg = .white, .bg = .black, .c = ' ', .bold = false }); 223 | try display.setPixel(config.UI_XOFF + 6 * x + 2, config.UI_YOFF + 3 * y, .{ .fg = .white, .bg = .black, .c = ' ', .bold = false }); 224 | try display.setPixel(config.UI_XOFF + 6 * x + 3, config.UI_YOFF + 3 * y, .{ .fg = .white, .bg = .black, .c = ' ', .bold = false }); 225 | 226 | try display.setPixel(config.UI_XOFF + 6 * x + 0, config.UI_YOFF + 3 * y + 1, .{ .fg = .white, .bg = .black, .c = ' ', .bold = false }); 227 | try display.setPixel(config.UI_XOFF + 6 * x + 1, config.UI_YOFF + 3 * y + 1, .{ .fg = .white, .bg = .black, .c = ' ', .bold = false }); 228 | try display.setPixel(config.UI_XOFF + 6 * x + 2, config.UI_YOFF + 3 * y + 1, .{ .fg = .white, .bg = .black, .c = ' ', .bold = false }); 229 | try display.setPixel(config.UI_XOFF + 6 * x + 3, config.UI_YOFF + 3 * y + 1, .{ .fg = .white, .bg = .black, .c = ' ', .bold = false }); 230 | } 231 | } 232 | } 233 | } 234 | 235 | fn drawPawn(display: *Display, x: usize, y: usize, c: color.Color) void { 236 | if (config.mini) { 237 | try display.setPixel(config.UI_XOFF + 4 * x + config.label_extra_w, config.UI_YOFF + 2 * y, .{ .fg = .white, .bg = c, .c = ' ', .bold = false }); 238 | try display.setPixel(config.UI_XOFF + 4 * x + 1 + config.label_extra_w, config.UI_YOFF + 2 * y, .{ .fg = .white, .bg = c, .c = ' ', .bold = false }); 239 | } else { 240 | try display.setPixel(config.UI_XOFF + 6 * x + 0, config.UI_YOFF + 3 * y, .{ .fg = .white, .bg = c, .c = ' ', .bold = false }); 241 | try display.setPixel(config.UI_XOFF + 6 * x + 1, config.UI_YOFF + 3 * y, .{ .fg = .white, .bg = c, .c = ' ', .bold = false }); 242 | try display.setPixel(config.UI_XOFF + 6 * x + 2, config.UI_YOFF + 3 * y, .{ .fg = .white, .bg = c, .c = ' ', .bold = false }); 243 | try display.setPixel(config.UI_XOFF + 6 * x + 3, config.UI_YOFF + 3 * y, .{ .fg = .white, .bg = c, .c = ' ', .bold = false }); 244 | 245 | try display.setPixel(config.UI_XOFF + 6 * x + 0, config.UI_YOFF + 3 * y + 1, .{ .fg = .white, .bg = c, .c = ' ', .bold = false }); 246 | try display.setPixel(config.UI_XOFF + 6 * x + 1, config.UI_YOFF + 3 * y + 1, .{ .fg = .white, .bg = c, .c = ' ', .bold = false }); 247 | try display.setPixel(config.UI_XOFF + 6 * x + 2, config.UI_YOFF + 3 * y + 1, .{ .fg = .white, .bg = c, .c = ' ', .bold = false }); 248 | try display.setPixel(config.UI_XOFF + 6 * x + 3, config.UI_YOFF + 3 * y + 1, .{ .fg = .white, .bg = c, .c = ' ', .bold = false }); 249 | } 250 | } 251 | 252 | fn drawFence(display: *Display, x: usize, y: usize, c: color.Color, dir: Dir) void { 253 | // x,y is most NW square adjacent to fence 254 | if (config.mini) { 255 | if (dir == .horz) { 256 | for (0..6) |xi| { 257 | try display.setPixel(xi + config.UI_XOFF + 4 * x + config.label_extra_w, config.UI_YOFF + 2 * y + 1, .{ .fg = .white, .bg = c, .c = ' ', .bold = false }); 258 | } 259 | } else { 260 | for (0..3) |yi| { 261 | try display.setPixel(config.UI_XOFF + 4 * x + 2 + config.label_extra_w, yi + config.UI_YOFF + 2 * y, .{ .fg = .white, .bg = c, .c = ' ', .bold = false }); 262 | try display.setPixel(config.UI_XOFF + 4 * x + 2 + 1 + config.label_extra_w, yi + config.UI_YOFF + 2 * y, .{ .fg = .white, .bg = c, .c = ' ', .bold = false }); 263 | } 264 | } 265 | } else { 266 | if (dir == .horz) { 267 | for (0..10) |xi| { 268 | try display.setPixel(config.UI_XOFF + 6 * x + xi, config.UI_YOFF + 3 * y + 2, .{ .fg = .white, .bg = c, .c = ' ', .bold = false }); 269 | } 270 | } else { 271 | for (0..5) |yi| { 272 | try display.setPixel(config.UI_XOFF + 6 * x + 4, config.UI_YOFF + 3 * y + yi, .{ .fg = .white, .bg = c, .c = ' ', .bold = false }); 273 | try display.setPixel(config.UI_XOFF + 6 * x + 5, config.UI_YOFF + 3 * y + yi, .{ .fg = .white, .bg = c, .c = ' ', .bold = false }); 274 | } 275 | } 276 | } 277 | } 278 | 279 | 280 | pub fn emitMoves(turnN: usize, moves: [2]Move) !void { 281 | var b1: [16]u8 = undefined; 282 | var b2: [16]u8 = undefined; 283 | const s1 = try moves[0].toString(&b1); 284 | const s2 = try moves[1].toString(&b2); 285 | 286 | config.lastTurnStr = try std.fmt.bufPrint(&config.lastTurnBuf, "Turn: {d}. {s} {s}", .{ turnN + 1, s1, s2 }); 287 | } 288 | -------------------------------------------------------------------------------- /src/uiagenthuman.zig: -------------------------------------------------------------------------------- 1 | const clock = @import("clock.zig"); 2 | const config = @import("config.zig"); 3 | const GameState = @import("gamestate.zig").GameState; 4 | const PosDir = @import("gamestate.zig").PosDir; 5 | const PosPath = @import("gamestate.zig").PosPath; 6 | const Pos = @import("gamestate.zig").Pos; 7 | const Dir = @import("gamestate.zig").Dir; 8 | const Move = @import("gamestate.zig").Move; 9 | const VerifiedMove = @import("gamestate.zig").VerifiedMove; 10 | const mibu = @import("mibu"); 11 | const events = mibu.events; 12 | const term = mibu.term; 13 | const color = mibu.color; 14 | const std = @import("std"); 15 | const Display = @import("display.zig").Display; 16 | 17 | const UiState = enum { 18 | Idle, 19 | MovingPawn, 20 | MovingFence, 21 | Completed, 22 | }; 23 | 24 | pub const UiAgentHuman = struct { 25 | const Self = @This(); 26 | state: UiState, 27 | nextMove: VerifiedMove, 28 | 29 | pub fn init() Self { 30 | return Self{ 31 | .state = .Idle, 32 | .nextMove = undefined, 33 | }; 34 | } 35 | 36 | fn selectMoveInteractivePawn(self: *Self, gs: *const GameState, pi: usize) !void { 37 | self.state = .MovingPawn; 38 | const move = Move{ .pawn = gs.pawns[pi].pos }; 39 | self.nextMove = try gs.verifyMove(pi, move); 40 | } 41 | 42 | fn selectMoveInteractiveFence(self: *Self, gs: *const GameState, pi: usize) !void { 43 | self.state = .MovingFence; 44 | const move = Move{ 45 | .fence = .{ // start fence placement in centre of grid 46 | .pos = .{ 47 | .x = config.GRIDSIZE / 2, 48 | .y = config.GRIDSIZE / 2, 49 | }, 50 | .dir = .horz, 51 | }, 52 | }; 53 | self.nextMove = try gs.verifyMove(pi, move); 54 | } 55 | 56 | pub fn selectMoveInteractive(self: *Self, gs: *const GameState, pi: usize) !void { 57 | // default to pawn first 58 | try self.selectMoveInteractivePawn(gs, pi); 59 | } 60 | 61 | pub fn getCompletedMove(self: *Self) ?VerifiedMove { 62 | switch (self.state) { 63 | .Completed => return self.nextMove, 64 | else => return null, 65 | } 66 | } 67 | 68 | pub fn process(self: *Self, gs: *const GameState, pi: usize) !void { 69 | _ = self; 70 | _ = gs; 71 | _ = pi; 72 | } 73 | 74 | pub fn handleEvent(self: *Self, event: events.Event, gs: *const GameState, pi: usize) !bool { 75 | switch (self.state) { 76 | .Completed => {}, 77 | .MovingFence => { 78 | switch (event) { 79 | .key => |k| switch (k) { 80 | .down => { 81 | if (self.nextMove.move.fence.pos.y + 1 < config.GRIDSIZE - 1) { 82 | self.nextMove.move.fence.pos.y += 1; 83 | self.nextMove = try gs.verifyMove(pi, self.nextMove.move); 84 | } 85 | }, 86 | .up => { 87 | if (self.nextMove.move.fence.pos.y > 0) { 88 | self.nextMove.move.fence.pos.y -= 1; 89 | self.nextMove = try gs.verifyMove(pi, self.nextMove.move); 90 | } 91 | }, 92 | .left => { 93 | if (self.nextMove.move.fence.pos.x > 0) { 94 | self.nextMove.move.fence.pos.x -= 1; 95 | self.nextMove = try gs.verifyMove(pi, self.nextMove.move); 96 | } 97 | }, 98 | .right => { 99 | if (self.nextMove.move.fence.pos.x + 1 < config.GRIDSIZE - 1) { 100 | self.nextMove.move.fence.pos.x += 1; 101 | self.nextMove = try gs.verifyMove(pi, self.nextMove.move); 102 | } 103 | }, 104 | .enter => { 105 | if (self.nextMove.legal) { 106 | self.state = .Completed; 107 | } 108 | }, 109 | .ctrl => |c| switch (c) { 110 | 'i' => { // tab 111 | try self.selectMoveInteractivePawn(gs, pi); 112 | }, 113 | else => {}, 114 | }, 115 | .char => |c| switch (c) { 116 | ' ' => { 117 | self.nextMove.move.fence.dir = self.nextMove.move.fence.dir.flip(); 118 | }, 119 | else => {}, 120 | }, 121 | else => {}, 122 | }, 123 | else => {}, 124 | } 125 | }, 126 | .MovingPawn => { 127 | // lowest x,y for movement allowed to avoid going offscreen 128 | var minx: usize = 0; 129 | if (gs.pawns[pi].pos.x > 1) { 130 | minx = gs.pawns[pi].pos.x - config.PAWN_EXPLORE_DIST; 131 | } 132 | var miny: usize = 0; 133 | if (gs.pawns[pi].pos.y > 1) { 134 | miny = gs.pawns[pi].pos.y - config.PAWN_EXPLORE_DIST; 135 | } 136 | 137 | switch (event) { 138 | .key => |k| switch (k) { 139 | .left => { 140 | if (self.nextMove.move.pawn.x > 0) { 141 | if (self.nextMove.move.pawn.x - 1 >= minx) { 142 | self.nextMove.move.pawn.x -= 1; 143 | self.nextMove = try gs.verifyMove(pi, self.nextMove.move); 144 | } 145 | } 146 | }, 147 | .right => { 148 | if (self.nextMove.move.pawn.x < config.GRIDSIZE - 1) { 149 | if (self.nextMove.move.pawn.x + 1 <= gs.pawns[pi].pos.x + config.PAWN_EXPLORE_DIST) { 150 | self.nextMove.move.pawn.x += 1; 151 | self.nextMove = try gs.verifyMove(pi, self.nextMove.move); 152 | } 153 | } 154 | }, 155 | .up => { 156 | if (self.nextMove.move.pawn.y > 0) { 157 | if (self.nextMove.move.pawn.y - 1 >= miny) { 158 | self.nextMove.move.pawn.y -= 1; 159 | self.nextMove = try gs.verifyMove(pi, self.nextMove.move); 160 | } 161 | } 162 | }, 163 | .down => { 164 | if (self.nextMove.move.pawn.y < config.GRIDSIZE - 1) { 165 | if (self.nextMove.move.pawn.y + 1 <= gs.pawns[pi].pos.y + config.PAWN_EXPLORE_DIST) { 166 | self.nextMove.move.pawn.y += 1; 167 | self.nextMove = try gs.verifyMove(pi, self.nextMove.move); 168 | } 169 | } 170 | }, 171 | .enter => { 172 | if (self.nextMove.legal) { 173 | self.state = .Completed; 174 | } 175 | }, 176 | .ctrl => |c| switch (c) { 177 | 'i' => { // tab 178 | if (gs.pawns[pi].numFencesRemaining > 0) { 179 | try self.selectMoveInteractiveFence(gs, pi); 180 | } 181 | }, 182 | else => {}, 183 | }, 184 | else => {}, 185 | }, 186 | else => {}, 187 | } 188 | }, 189 | .Idle => {}, 190 | } 191 | return true; 192 | } 193 | 194 | pub fn paint(self: *Self, display: *Display) !void { 195 | switch (self.state) { 196 | .Completed => {}, 197 | .MovingPawn => { 198 | if (self.nextMove.legal) { 199 | drawPawnHighlight(display, self.nextMove.move.pawn.x, self.nextMove.move.pawn.y, .green); 200 | } else { 201 | drawPawnHighlight(display, self.nextMove.move.pawn.x, self.nextMove.move.pawn.y, .red); 202 | } 203 | }, 204 | .MovingFence => { 205 | if ((clock.millis() / 100) % 5 > 0) { // flash highlight 206 | if (self.nextMove.legal) { 207 | drawFenceHighlight(display, self.nextMove.move.fence.pos.x, self.nextMove.move.fence.pos.y, .white, self.nextMove.move.fence.dir); 208 | } else { 209 | drawFenceHighlight(display, self.nextMove.move.fence.pos.x, self.nextMove.move.fence.pos.y, .red, self.nextMove.move.fence.dir); 210 | } 211 | } 212 | }, 213 | .Idle => {}, 214 | } 215 | } 216 | }; 217 | 218 | fn drawPawnHighlight(display: *Display, x: usize, y: usize, c: color.Color) void { 219 | if (config.mini) { 220 | try display.setPixel(config.UI_XOFF + 4 * x + config.label_extra_w - 1, config.UI_YOFF + 2 * y, .{ .fg = .white, .bg = c, .c = '[', .bold = false }); 221 | try display.setPixel(config.UI_XOFF + 4 * x + config.label_extra_w + 2, config.UI_YOFF + 2 * y, .{ .fg = .white, .bg = c, .c = ']', .bold = false }); 222 | } else { 223 | try display.setPixel(config.UI_XOFF + 6 * x - 1, config.UI_YOFF + 3 * y, .{ .fg = .white, .bg = c, .c = ' ', .bold = false }); 224 | try display.setPixel(config.UI_XOFF + 6 * x + 4, config.UI_YOFF + 3 * y, .{ .fg = .white, .bg = c, .c = ' ', .bold = false }); 225 | try display.setPixel(config.UI_XOFF + 6 * x - 1, config.UI_YOFF + 3 * y + 1, .{ .fg = .white, .bg = c, .c = ' ', .bold = false }); 226 | try display.setPixel(config.UI_XOFF + 6 * x + 4, config.UI_YOFF + 3 * y + 1, .{ .fg = .white, .bg = c, .c = ' ', .bold = false }); 227 | } 228 | } 229 | 230 | fn drawFenceHighlight(display: *Display, x: usize, y: usize, c: color.Color, dir: Dir) void { 231 | if (config.mini) { 232 | // x,y is most NW square adjacent to fence 233 | if (dir == .horz) { 234 | for (0..6) |xi| { 235 | try display.setPixel(xi + config.UI_XOFF + 4 * x + config.label_extra_w, config.UI_YOFF + 2 * y + 1, .{ .fg = .white, .bg = c, .c = ' ', .bold = false }); 236 | } 237 | } else { 238 | for (0..3) |yi| { 239 | try display.setPixel(config.UI_XOFF + 4 * x + 2 + config.label_extra_w, yi + config.UI_YOFF + 2 * y, .{ .fg = .white, .bg = c, .c = ' ', .bold = false }); 240 | try display.setPixel(config.UI_XOFF + 4 * x + 2 + 1 + config.label_extra_w, yi + config.UI_YOFF + 2 * y, .{ .fg = .white, .bg = c, .c = ' ', .bold = false }); 241 | } 242 | } 243 | } else { 244 | if (dir == .horz) { 245 | for (0..10) |xi| { 246 | try display.setPixel(config.UI_XOFF + 6 * x + xi, config.UI_YOFF + 3 * y + 2, .{ .fg = .white, .bg = c, .c = ' ', .bold = false }); 247 | } 248 | } else { 249 | for (0..5) |yi| { 250 | try display.setPixel(config.UI_XOFF + 6 * x + 4, config.UI_YOFF + 3 * y + yi, .{ .fg = .white, .bg = c, .c = ' ', .bold = false }); 251 | try display.setPixel(config.UI_XOFF + 6 * x + 5, config.UI_YOFF + 3 * y + yi, .{ .fg = .white, .bg = c, .c = ' ', .bold = false }); 252 | } 253 | } 254 | } 255 | } 256 | 257 | -------------------------------------------------------------------------------- /src/uiagentmachine.zig: -------------------------------------------------------------------------------- 1 | const clock = @import("clock.zig"); 2 | const config = @import("config.zig"); 3 | const GameState = @import("gamestate.zig").GameState; 4 | const PosDir = @import("gamestate.zig").PosDir; 5 | const PosPath = @import("gamestate.zig").PosPath; 6 | const Pos = @import("gamestate.zig").Pos; 7 | const Dir = @import("gamestate.zig").Dir; 8 | const Move = @import("gamestate.zig").Move; 9 | const VerifiedMove = @import("gamestate.zig").VerifiedMove; 10 | const mibu = @import("mibu"); 11 | const events = mibu.events; 12 | const term = mibu.term; 13 | const color = mibu.color; 14 | const std = @import("std"); 15 | const Display = @import("display.zig").Display; 16 | 17 | var prng: std.Random.Xoshiro256 = undefined; 18 | var rand: std.Random = undefined; 19 | var randInited = false; 20 | 21 | const UiState = enum { 22 | Idle, 23 | Processing, 24 | Completed, 25 | }; 26 | 27 | pub const UiAgentMachine = struct { 28 | const Self = @This(); 29 | state: UiState, 30 | nextMove: VerifiedMove, 31 | 32 | pub fn init() Self { 33 | if (!randInited) { 34 | randInited = true; 35 | if (config.RANDOMSEED) |seed| { 36 | prng = std.Random.DefaultPrng.init(@intCast(seed)); 37 | } else { 38 | prng = std.Random.DefaultPrng.init(@intCast(clock.millis())); 39 | } 40 | rand = prng.random(); 41 | } 42 | 43 | return Self{ 44 | .state = .Idle, 45 | .nextMove = undefined, 46 | }; 47 | } 48 | 49 | pub fn paint(self: *Self, display: *Display) !void { 50 | _ = self; 51 | _ = display; 52 | } 53 | 54 | fn calcPathlen(gs: *const GameState, pi: usize) !usize { 55 | var pathbuf: PosPath = undefined; 56 | if (gs.findShortestPath(pi, gs.getPawnPos(pi), &pathbuf)) |path| { 57 | return path.len; 58 | } else { 59 | // std.debug.print("pi = {any}\r\n", .{pi}); 60 | // std.debug.print("graph = {any}\r\n", .{gs.graph}); 61 | // std.debug.print("pawns = {any}\r\n", .{gs.pawns}); 62 | // std.debug.print("fences = {any}\r\n", .{gs.fences}); 63 | // std.debug.print("numFences = {any}\r\n", .{gs.numFences}); 64 | return error.InvalidMoveErr; 65 | } 66 | } 67 | 68 | fn scoreMove(self: *Self, _gs: *const GameState, pi: usize, move: Move) !usize { 69 | // Calculate an estimated score for potential move, minimax only looking at one move ahead 70 | // Calculates lengths of my and opponents shortest paths to goal 71 | // Wins points if this move shortens mine and lengthens theirs 72 | // Slight scoring bonus for heading towards goal, to tie break equally scored moves 73 | // Slight scoring bonus for lengthening opponents shortest path to goal 74 | _ = self; 75 | var gs = _gs.*; // clone gamestate 76 | 77 | const myPathlenPre = try calcPathlen(&gs, pi); 78 | const oppPathlenPre = try calcPathlen(&gs, (pi + 1) % config.NUM_PAWNS); 79 | const myScorePre: isize = @as(isize, @intCast(oppPathlenPre)) - @as(isize, @intCast(myPathlenPre)); // +ve if I'm closer 80 | 81 | const goalDistPre: isize = @as(isize, @intCast(gs.pawns[pi].pos.y)) - @as(isize, @intCast(gs.pawns[pi].goaly)); 82 | 83 | const vm = VerifiedMove{ .move = move, .legal = true }; // we know it's safe 84 | 85 | try gs.applyMove(pi, vm); // move in clone 86 | if (gs.hasWon(pi)) { // top score for winning move 87 | return 999999; 88 | } 89 | 90 | const myPathlenPost = try calcPathlen(&gs, pi); 91 | const oppPathlenPost = try calcPathlen(&gs, (pi + 1) % config.NUM_PAWNS); 92 | const myScorePost: isize = @as(isize, @intCast(oppPathlenPost)) - @as(isize, @intCast(myPathlenPost)); // +ve if I'm closer 93 | 94 | const scoreDel: isize = myScorePost - myScorePre; 95 | 96 | // add a small bonus if reduces my distance to goal 97 | const goalDistPost: isize = @as(isize, @intCast(gs.pawns[pi].pos.y)) - @as(isize, @intCast(gs.pawns[pi].goaly)); 98 | const goalDistDel = @as(isize, @intCast(@abs(goalDistPre))) - @as(isize, @intCast(@abs(goalDistPost))); 99 | 100 | // small bonus if increases their pathlen 101 | var r: isize = 0; 102 | if (myScorePre < 0) { // if I'm losing 103 | if (oppPathlenPost > oppPathlenPre) { // and this move lengthens their path 104 | r = 100; // give it a bonus 105 | } 106 | } 107 | 108 | if (config.RANDOMNESS > 0) { 109 | // perturb score by randomness factor 110 | r += @intCast(rand.int(u32) % config.RANDOMNESS); 111 | } 112 | 113 | // +100000 is to ensure no result is negative 114 | return @as(usize, @intCast((scoreDel * 100) + 100000 + (goalDistDel * 10) + r)); 115 | } 116 | 117 | pub fn process(self: *Self, gs: *const GameState, pi: usize) !void { 118 | switch (self.state) { 119 | .Idle, .Completed => {}, 120 | .Processing => { // generating a move 121 | var moves: [config.MAXMOVES]Move = undefined; 122 | var scores: [config.MAXMOVES]usize = undefined; 123 | var bestScore: usize = 0; 124 | var bestScoreIndex: usize = 0; 125 | 126 | if (gs.hasGameEnded()) { 127 | self.state = .Idle; 128 | return; 129 | } 130 | 131 | // generate all legal moves 132 | const numMoves = try gs.getAllLegalMoves(pi, &moves, 0); 133 | // score them all 134 | for (0..numMoves) |i| { 135 | scores[i] = try self.scoreMove(gs, pi, moves[i]); 136 | //std.debug.print("SCORE = {d} MOVE = {any}\r\n", .{scores[i], moves[i]}); 137 | if (scores[i] > bestScore) { 138 | bestScoreIndex = i; 139 | bestScore = scores[i]; 140 | } 141 | } 142 | //std.debug.print("numMoves={d} SCORE = {d} BESTMOVE = {any}\r\n", .{numMoves, bestScore, moves[bestScoreIndex]}); 143 | //gs.print(); 144 | // play highest scoring move 145 | self.nextMove = try gs.verifyMove(pi, moves[bestScoreIndex]); 146 | if (!self.nextMove.legal) { 147 | //std.debug.print("move = {any}\r\n", .{self.nextMove}); 148 | return error.InvalidMoveErr; 149 | } 150 | self.state = .Completed; 151 | }, 152 | } 153 | } 154 | 155 | pub fn handleEvent(self: *Self, event: events.Event, gs: *const GameState, pi: usize) !bool { 156 | _ = gs; 157 | _ = pi; 158 | _ = self; 159 | _ = event; 160 | return false; 161 | } 162 | 163 | pub fn selectMoveInteractive(self: *Self, gs: *const GameState, pi: usize) !void { 164 | _ = gs; 165 | _ = pi; 166 | self.state = .Processing; 167 | } 168 | 169 | pub fn getCompletedMove(self: *Self) ?VerifiedMove { 170 | switch (self.state) { 171 | .Completed => return self.nextMove, 172 | else => return null, 173 | } 174 | } 175 | }; 176 | 177 | 178 | -------------------------------------------------------------------------------- /src/uiagentrandom.zig: -------------------------------------------------------------------------------- 1 | const config = @import("config.zig"); 2 | const GameState = @import("gamestate.zig").GameState; 3 | const Move = @import("gamestate.zig").Move; 4 | const VerifiedMove = @import("gamestate.zig").VerifiedMove; 5 | const mibu = @import("mibu"); 6 | const events = mibu.events; 7 | const std = @import("std"); 8 | const Display = @import("display.zig").Display; 9 | const clock = @import("clock.zig"); 10 | 11 | var prng: std.Random.Xoshiro256 = undefined; 12 | var rand: std.Random = undefined; 13 | var randInited = false; 14 | 15 | const UiState = enum { 16 | Idle, 17 | Processing, 18 | Completed, 19 | }; 20 | 21 | pub const UiAgentRandom = struct { 22 | const Self = @This(); 23 | state: UiState, 24 | nextMove: VerifiedMove, 25 | 26 | pub fn init() Self { 27 | if (!randInited) { 28 | randInited = true; 29 | if (config.RANDOMSEED) |seed| { 30 | prng = std.Random.DefaultPrng.init(@intCast(seed)); 31 | } else { 32 | prng = std.Random.DefaultPrng.init(@intCast(clock.millis())); 33 | } 34 | rand = prng.random(); 35 | } 36 | 37 | return Self{ 38 | .state = .Idle, 39 | .nextMove = undefined, 40 | }; 41 | } 42 | 43 | pub fn paint(self: *Self, display: *Display) !void { 44 | _ = self; 45 | _ = display; 46 | } 47 | 48 | pub fn getAnyLegalMove(self: *const Self, gs: *const GameState, pi: usize) !VerifiedMove{ 49 | _ = self; 50 | while(true) { 51 | const move = switch(rand.int(usize) % 3) { 52 | 0 => Move{ .pawn = .{ .x = @intCast(rand.int(usize)%9), .y = @intCast(rand.int(usize)%9) } }, 53 | 1 => Move{ .fence = .{ .pos = .{ .x = @intCast(rand.int(usize)%8), .y = @intCast(rand.int(usize)%8) }, .dir = .vert } }, 54 | 2 => Move{ .fence = .{ .pos = .{ .x = @intCast(rand.int(usize)%8), .y = @intCast(rand.int(usize)%8) }, .dir = .horz } }, 55 | else => unreachable, 56 | }; 57 | const vm = try gs.verifyMove(pi, move); 58 | if (vm.legal) { 59 | return vm; 60 | } 61 | } 62 | } 63 | 64 | pub fn handleEvent(self: *Self, event: ?events.Event, gs: *const GameState, pi: usize) !bool { 65 | _ = gs; 66 | _ = pi; 67 | _ = self; 68 | _ = event; 69 | return false; 70 | } 71 | 72 | pub fn process(self: *Self, gs: *const GameState, pi: usize) !void { 73 | switch (self.state) { 74 | .Idle, .Completed => {}, 75 | .Processing => { // generating a move 76 | self.nextMove = try self.getAnyLegalMove(gs, pi); 77 | self.state = .Completed; 78 | }, 79 | } 80 | } 81 | 82 | pub fn selectMoveInteractive(self: *Self, gs: *const GameState, pi: usize) !void { 83 | _ = gs; 84 | _ = pi; 85 | self.state = .Processing; 86 | } 87 | 88 | pub fn getCompletedMove(self: *Self) ?VerifiedMove { 89 | switch (self.state) { 90 | .Completed => return self.nextMove, 91 | else => return null, 92 | } 93 | } 94 | }; 95 | 96 | 97 | -------------------------------------------------------------------------------- /src/wasm4.css: -------------------------------------------------------------------------------- 1 | html,body{height:100%;margin:0}@font-face{font-family:wasm4-font;src:url(data:font/woff2;base64,d09GMgABAAAAAAegAA0AAAAAKHwAAAdOAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYGYAA0CAQRCArGHLlRC4FCAAE2AiQDgUIEIAWDJgcgG4ggUVRQhkQRbBylAWCR/eGAmzBgcbVSEQV1MFRs8Ts/TVirjdcmzZum+foZqYGq4TdA2FbzSDx8f5+ee7Ns74fim/BWoE4AP1Wwc6lakA20VO54RMrR2gpfsF97s/P3xKMD8UIkBZNFk2ondPz/n9v9G4ROvGcJ9ejaJw8RK4kYUOt8zO4KgUDgofBQGDwMCjsf2FAHKMuSxfQKGICKTqpw8TAYfHywFNs02zbbJDAgUAD+z7l4byADjcSJjXhAkX+V15z5PA0HfmEHdHqW5OfXT1PsVLlV5AkPgm14EPW/1ds0nWuhRkHhmb/7JBfdZazTpbTGMrxynHPs9XO634fF7LHGGYvKrld7xSsgpTyfJqABJJGiNEP0JXXyPy/gUd+Oq0QckHKAEocXne/KDv7oOwSG0MMrDDRgQ0HCAFZ0Ar7LFhgi+t39/2Lzv/+fFLDLD/G/EugBH+CBbj+wXA/giv6ofkm3ZAqiNhrXlgkd4AIQwJg18/ES0QEKIZXedNDpbH9FmDzLD3D97q2rzT3TRXdngPKMRazVYXWiUtlpVnZTprWt4J6wbQiLK7o/u+vhZr91OWHc2/gH+Qyq505NGWB7ui1DWkl94VWLPsznaf1Socl++32X0B1fLASbyLYi6TAO6LlJoPwXg5YAIDQZ2GxXd7yaLzKdTNrAjkIbD1Sca4fUrWmNr+IUViUJVbuuu66KDZXmnH4fMCQ5ukPCRJDdgit52b4Et5RuCK2ZjuBXWu6K6zJkW4qAxA6Y0BWJpS9UgZPLQ3iSuQWAodKrD1VK44bhgApzKGely9pyBS45qWsGAx7ELrGRNkqZX2rpG1no3KgrF9zv29HXdcAmEYpTeqXQunmcM1KNLri1l3ETKhr/jkuZpYQWCtMUkAOA+G31Ezc8TlQCcX0AFiJM8A/Ja5ANNF3s+Aj1HaR91PPZwCslbEBH81Bta7KBGjhAdpteqxIHxKrVrglxhbYG0qRrxPwYd+RA3LtpvfDZhqQKKExdOI4SSCr8hboPCCdUxuYcsLQ2TWw8XO+W8S8kg28tTsGhGLgAGRwq7qpSd5XiJn8+0m7km8BBpAhQ5rqdtpQ58nsk6UVIA1DbFmX+klm8NFI+mk+Ll5BXDAY1ZsGRNeDGIUiLnIISx8u1n++kPlx5KbJLJOSiIDP1dfY4drhKlPE7oTwc9gEI5vBFDpQ2ih/QCB4V+GrFeAMbdFltBSpSqZm9jTuVVM1CVTyG+Haj1uZhNOXIU0xda+WccPIV0vAyy13AXHS7odKqX6lPnNQW29uptFF7hjud+bei1obGQ3mmpvE7YI+21Nlt7iq+tVb3XTcUFG5q31u2uVI1qvk971yZ+/NvzpZV1dNa7Wrems+vXZ7G0bjYrBMdM44xvrdfytxyouoAmPMIUnc0H1njzFfrgVdARZ2VnWXVw+0qv/fX8iYj698zzrSTOlorbpYF7dsMJFE7yW5kV/NV/wwJ/6VW+S3vk6u7s/frQR4kpi7JSQUksyJhl0BSNPS65eUmD+i8UGEYHhd5zmQA0oU5xN+qs8fdwfJZAQaqNDFTgSowOASAVTi4nNRD5CwJVyFK13cHxDEeWVQDvZU3CfsgXuwhIGxxAGE3sxM1QNCL5oTRpAtVec0/pJXNfeLQLO+973quQzeYBhK7GMFx18+4DtE1AX90lFeaawcfPwKbh9CZncWGRig514Ca6r5aIcMk1u7R7OyRfIjVldpkv/8QTig8xMfDoNqXMmPq6rWqoKMWvmXNIhNcLI1TokIhQLNwOGTfZxXWtBtNJFOwSQNl+3DGVwiBVU4+Oq0FeJi5E8VTE1ABD05R60ZWTc49DS4M1nMCCzmZSm7M8UWDSoiGvZPrWrGYd4bKvbh8oXu1Pnv2vrfKud0bz5t2kT7Ti8FNP4L9IJy/PdAULtKZAlxz2FahwUZvcI6aZm61UI4qEJ3XUpHb3NZcIBs7HYrbHf7GgFYvCv4JTMlVK5o9e10zmwVpEoUbhD8pWxVbCX6xdKM4RGTiZ/2PaYEZMh7uAHi1vTTwNaeeqsmWeoX+fERAY8Dlq8IlZUVTrzi35D7ANkN8liq6UConAkh2mBUCA+jNZ4cnuXvJDuFGvEUlaWfzT5MjGonslkNYRjXtmwJEQgbrm1xCr4/AZMWbVPGZ3SvOH9q/AaXqXNGRhP+VuL2mvmu8/zv9BnB5l14cMu1MBSJ04++6QDnTv1bPp6oZnRul1ORTFVkMUlS1LsSh2h2L41VW44wWXcvRhJYwoLfn7pCnX7rB3Zs4otOwC568tbQs/egw8kfbjvhpM06nOc/rWqbg7EicnAydmY7O4nomxNHJws4WxYXFJFGl55716GhGRqqjKI9kp3x0+aiPQqzthaSFKiR6LmZAzFysLXdGb3rZfIi6TEU7jgcy2geINtNWQpaBCnYWTmxqWDw0wzgfrJvzphPhoIstcvGIzuZO4UN1requPbF1HWhqLtgPf3YiwnTA/xIgKAA=)} 2 | -------------------------------------------------------------------------------- /src/webmain.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const Display = @import("display.zig").Display; 4 | 5 | const clock = @import("clock.zig"); 6 | const config = @import("config.zig"); 7 | 8 | const GameState = @import("gamestate.zig").GameState; 9 | const GameRecord = @import("record.zig").GameRecord; 10 | const Move = @import("gamestate.zig").Move; 11 | 12 | const UiAgent = @import("ui.zig").UiAgent; 13 | const UiAgentHuman = @import("uiagenthuman.zig").UiAgentHuman; 14 | const UiAgentMachine = @import("uiagentmachine.zig").UiAgentMachine; 15 | const drawGame = @import("ui.zig").drawGame; 16 | const emitMoves = @import("ui.zig").emitMoves; 17 | 18 | const buildopts = @import("buildopts"); 19 | 20 | const console = @import("console.zig").getWriter().writer(); 21 | 22 | const WebState = struct { 23 | pi: usize, 24 | gs: GameState, 25 | record: GameRecord, 26 | recordB64Buf: ?[]const u8, // base64 of all moves performed so far 27 | startB64Buf: [1024]u8, // buffer to setup initial game state 28 | start: ?[]u8, 29 | }; 30 | var wstate:WebState = undefined; 31 | var inited = false; 32 | 33 | fn updateRecord(move:Move) !void { 34 | try wstate.record.append(move); 35 | // generate base64 for game 36 | if (wstate.recordB64Buf) |buf| { 37 | std.heap.page_allocator.free(buf); 38 | wstate.recordB64Buf = null; 39 | } 40 | wstate.recordB64Buf = try wstate.record.toStringBase64Alloc(std.heap.page_allocator); 41 | } 42 | 43 | // exposed to JS 44 | fn getNextMoveInternal() !void { 45 | wstate.pi = (wstate.pi + 1) % config.NUM_PAWNS; 46 | 47 | // FIXME assumes move is available immediately, js should poll for it and call process repeatedly 48 | try config.players[wstate.pi].selectMoveInteractive(&wstate.gs, wstate.pi); 49 | try config.players[wstate.pi].process(&wstate.gs, wstate.pi); 50 | if (config.players[wstate.pi].getCompletedMove()) |vmove| { 51 | try wstate.gs.applyMove(wstate.pi, vmove); 52 | try updateRecord(vmove.move); 53 | // next player 54 | wstate.pi = (wstate.pi + 1) % config.NUM_PAWNS; 55 | } 56 | } 57 | 58 | export fn isFenceMoveLegal(x:usize, y:usize, dir:u8) bool { 59 | const move = Move{ .fence = .{ .pos = .{ .x = @intCast(x), .y = @intCast(y) }, .dir = if (dir=='v') .vert else .horz } }; 60 | const vm = try wstate.gs.verifyMove(wstate.pi, move); 61 | return vm.legal; 62 | } 63 | 64 | export fn isPawnMoveLegal(x:usize, y:usize) bool { 65 | const move = Move{ .pawn = .{ .x = @intCast(x), .y = @intCast(y) } }; 66 | const vm = try wstate.gs.verifyMove(wstate.pi, move); 67 | return vm.legal; 68 | } 69 | 70 | export fn moveFence(x:usize, y:usize, dir:u8) void { 71 | const move = Move{ .fence = .{ .pos = .{ .x = @intCast(x), .y = @intCast(y) }, .dir = if (dir=='v') .vert else .horz } }; 72 | const vm = try wstate.gs.verifyMove(wstate.pi, move); 73 | try wstate.gs.applyMove(wstate.pi, vm); 74 | _ = updateRecord(vm.move) catch 0; 75 | 76 | // move opponent 77 | _ = getNextMoveInternal() catch 0; 78 | } 79 | 80 | export fn movePawn(x:usize, y:usize) void { 81 | const move = Move{ .pawn = .{ .x = @intCast(x), .y = @intCast(y) } }; 82 | const vm = try wstate.gs.verifyMove(wstate.pi, move); 83 | try wstate.gs.applyMove(wstate.pi, vm); 84 | _ = updateRecord(vm.move) catch 0; 85 | 86 | // move opponent 87 | _ = getNextMoveInternal() catch 0; 88 | } 89 | 90 | export fn getPlayerIndex() usize { 91 | return wstate.pi; 92 | } 93 | 94 | export fn hasWon(pi:usize) bool { 95 | return wstate.gs.hasWon(pi); 96 | } 97 | 98 | export fn getNumFences() usize { 99 | return wstate.gs.numFences; 100 | } 101 | export fn getFencePosX(i:usize) usize { 102 | return wstate.gs.fences[i].pos.x; 103 | } 104 | export fn getFencePosY(i:usize) usize { 105 | return wstate.gs.fences[i].pos.y; 106 | } 107 | export fn getFencePosDir(i:usize) usize { 108 | return switch(wstate.gs.fences[i].dir) { 109 | .vert => 'v', 110 | .horz => 'h', 111 | }; 112 | } 113 | 114 | export fn getPawnPosX(pi:usize) usize { 115 | return wstate.gs.getPawnPos(pi).x; 116 | } 117 | export fn getPawnPosY(pi:usize) usize { 118 | return wstate.gs.getPawnPos(pi).y; 119 | } 120 | 121 | export fn getFencesRemaining(pi:usize) usize { 122 | return wstate.gs.pawns[pi].numFencesRemaining; 123 | } 124 | 125 | export fn restart(pi:usize, setupB64Len:u32) bool { 126 | const b64input = wstate.startB64Buf[0..setupB64Len]; 127 | //_ = console.print("RESTART setupB64Len={d} s={s}\n", .{setupB64Len, b64input}) catch 0; 128 | return gamesetup(pi, b64input) catch false; 129 | } 130 | 131 | export fn allocUint8(length: u32) [*]const u8 { 132 | const slice = std.heap.page_allocator.alloc(u8, length) catch 133 | @panic("failed to allocate memory"); 134 | return slice.ptr; 135 | } 136 | 137 | export fn getGameStartRecordLen() usize { 138 | return wstate.startB64Buf.len; 139 | } 140 | 141 | export fn getGameStartRecordPtr() [*]const u8 { 142 | return (&wstate.startB64Buf).ptr; 143 | } 144 | 145 | export fn getGameRecordPtr() [*]const u8 { 146 | if (wstate.recordB64Buf) |b| { 147 | return b.ptr; 148 | } else { 149 | return @ptrFromInt(0xDEADBEEF); // len will be 0, so ignored 150 | } 151 | } 152 | export fn getGameRecordLen() usize { 153 | if (wstate.recordB64Buf) |b| { 154 | return b.len; 155 | } else { 156 | return 0; 157 | } 158 | } 159 | 160 | fn gamesetup(piStart:usize, b64O:?[]const u8) !bool { 161 | if (inited) { 162 | wstate.record.deinit(); 163 | if (wstate.recordB64Buf != null) { 164 | std.heap.page_allocator.free(wstate.recordB64Buf.?); 165 | } 166 | } 167 | 168 | var pi = piStart; 169 | var gs = GameState.init(); 170 | var recordB64Buf:?[]const u8 = null; 171 | var record:GameRecord = undefined; 172 | if (b64O) |b64| { 173 | // user supplied starting state 174 | record = try GameRecord.initFromBase64(std.heap.page_allocator, b64); 175 | gs = try record.toGameState(true); 176 | recordB64Buf = try record.toStringBase64Alloc(std.heap.page_allocator); 177 | pi += record.getAllMoves().len % 2; 178 | } else { 179 | record = try GameRecord.init(std.heap.page_allocator); 180 | } 181 | 182 | wstate = .{ 183 | .pi = pi, 184 | .gs = gs, 185 | .record = record, 186 | .recordB64Buf = recordB64Buf, 187 | .startB64Buf = undefined, 188 | .start = null, 189 | }; 190 | inited = true; 191 | config.players[0] = try UiAgent.make("machine"); // should be "null" 192 | config.players[1] = try UiAgent.make("machine"); 193 | if (pi != 0) { 194 | _ = getNextMoveInternal() catch 0; 195 | } 196 | return true; 197 | } 198 | 199 | export fn init() void { 200 | _ = console.print("Hello world\n", .{}) catch 0; 201 | _ = gamesetup(0, null) catch 0; 202 | } 203 | 204 | pub fn logFn( 205 | comptime message_level: std.log.Level, 206 | comptime scope: @TypeOf(.enum_literal), 207 | comptime format: []const u8, 208 | args: anytype, 209 | ) void { 210 | _ = message_level; 211 | _ = scope; 212 | _ = console.print(format, args) catch 0; 213 | } 214 | 215 | pub const std_options: std.Options = .{ 216 | .logFn = logFn, 217 | }; 218 | 219 | pub fn panic(msg: []const u8, trace: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn { 220 | _ = ret_addr; 221 | _ = trace; 222 | _ = console.print("PANIC: {s}", .{msg}) catch 0; 223 | while (true) {} 224 | } 225 | 226 | pub fn main() !void { 227 | // default to human vs machine 228 | config.players[0] = try UiAgent.make("human"); 229 | config.players[1] = try UiAgent.make("machine"); 230 | 231 | clock.initTime(); 232 | } 233 | -------------------------------------------------------------------------------- /src/zoridor.js: -------------------------------------------------------------------------------- 1 | let globalInstance = null; 2 | let console_buffer = ''; 3 | 4 | 5 | function console_write(dataPtr, len) { 6 | const wasmMemoryArray = new Uint8Array(globalInstance.exports.memory.buffer); 7 | var arr = new Uint8Array(wasmMemoryArray.buffer, dataPtr, len); 8 | let string = new TextDecoder().decode(arr); 9 | 10 | console_buffer += string.toString('binary'); 11 | 12 | // force output of very long line 13 | if (console_buffer.length > 1024) { 14 | console.log(console_buffer); 15 | console_buffer = ''; 16 | } 17 | 18 | // break on lines 19 | let lines = console_buffer.split(/\r?\n/); 20 | if (lines.length > 1) { 21 | console_buffer = lines.pop(); 22 | lines.forEach(l => console.log(l)); 23 | } 24 | } 25 | 26 | function getTimeUs() { 27 | return window.performance.now() * 1000; 28 | } 29 | 30 | export class Zoridor { 31 | static cellSz = 25; // px 32 | static statsCellSz = 10; // px 33 | static statw = 24; 34 | static stath = 13; 35 | 36 | static colourBackground = 'rgb(175,138,100)'; 37 | static colourFrame = 'rgb(155,118,80)'; 38 | static colourPawnSquareEmpty = 'rgb(101,67,40)'; 39 | static colourFence = 'rgb(229,203,67)'; 40 | static colourFenceIllegal = null; 41 | static fenceRadius = '5px'; 42 | static pawnRadius = '50px'; 43 | static gridRadius = '1px'; 44 | static colourPawns = [ 45 | 'rgb(25,25,25)', 46 | 'rgb(215,30,40)' 47 | ]; 48 | static colourPawnsDim = [ 49 | 'rgb(80,60,35)', 50 | 'rgb(80,60,35)' 51 | ]; 52 | 53 | static colourPawnIllegal = null; 54 | 55 | static HORZ = 'h'.charCodeAt(0); 56 | static VERT = 'v'.charCodeAt(0); 57 | 58 | static gameOver = false; 59 | static fences = []; 60 | static pawns = []; 61 | static pi = 0; // which player's turn 62 | 63 | static recordElem = null; 64 | 65 | static getInstance() { 66 | return globalInstance; 67 | } 68 | 69 | static getGameRecord = () => { 70 | const len = globalInstance.exports.getGameRecordLen(); 71 | if (len > 0) { 72 | const wasmMemoryArray = new Uint8Array(globalInstance.exports.memory.buffer); 73 | var arr = new Uint8Array(wasmMemoryArray.buffer, globalInstance.exports.getGameRecordPtr(), len); 74 | let string = new TextDecoder().decode(arr); 75 | return string; 76 | } else { 77 | return ''; 78 | } 79 | }; 80 | 81 | static async init(wasmFile) { 82 | // fetch wasm and instantiate 83 | await fetch(wasmFile).then((response) => { 84 | return response.arrayBuffer(); 85 | }).then((bytes) => { 86 | let imports = { 87 | env: { 88 | console_write: console_write, 89 | getTimeUs: getTimeUs 90 | } 91 | }; 92 | return WebAssembly.instantiate(bytes, imports); 93 | }).then((results) => { 94 | let instance = results.instance; 95 | console.log("Instantiated wasm", instance.exports); 96 | globalInstance = instance; 97 | }).catch((err) => { 98 | console.log(err); 99 | }); 100 | } 101 | 102 | static restart(pi, b64) { 103 | this.gameOver = false; 104 | 105 | const wasmMemoryArray = new Uint8Array(globalInstance.exports.memory.buffer); 106 | if (b64.length > globalInstance.exports.getGameStartRecordLen()) { 107 | alert("too big"); 108 | } 109 | var arr = new Uint8Array(wasmMemoryArray.buffer, globalInstance.exports.getGameStartRecordPtr(), b64.length); 110 | arr.set(new TextEncoder().encode(b64)); 111 | 112 | if (!globalInstance.exports.restart(pi, b64.length)) { 113 | alert("bad data"); 114 | } 115 | this.fetchState(); 116 | this.drawPieces(); 117 | } 118 | 119 | static async start() { 120 | // tell wasm to start 121 | if (globalInstance.exports.init) { 122 | globalInstance.exports.init(); 123 | this.fetchState(); 124 | this.drawPieces(); 125 | } 126 | } 127 | 128 | static decodePos(x, y) { 129 | if (x<0 || x > 3*9 || y<0 || y>3*9) { 130 | return null; 131 | } 132 | let gx = Math.floor(x/3); // game coords 133 | let gy = Math.floor(y/3); 134 | 135 | if ((x % 3 == 0 || x % 3 == 1) && (y % 3 == 0 || y % 3 == 1)) { // in a pawn area 136 | return {pawn: {x: gx, y: gy}}; 137 | } 138 | 139 | if ((x % 3 == 2) && (y % 3 == 0 || y % 3 == 1)) { // vert fence 140 | if (gy > 7) { 141 | gy = 7; 142 | } 143 | // try the other half of the fence 144 | if (!globalInstance.exports.isFenceMoveLegal(gx, gy, this.VERT) && gy > 0) { 145 | gy -= 1; 146 | } 147 | return {fence: {x: gx, y: gy, dir: this.VERT}}; 148 | } 149 | 150 | if ((y % 3 == 2) && (x % 3 == 0 || x % 3 == 1)) { // horz fence 151 | if (gx > 7) { 152 | gx = 7; 153 | } 154 | // try the other half of the fence 155 | if (!globalInstance.exports.isFenceMoveLegal(gx, gy, this.HORZ) && gx > 0) { 156 | gx -= 1; 157 | } 158 | return {fence: {x: gx, y: gy, dir: this.HORZ}}; 159 | } 160 | 161 | // dead centre spot, move left giving horizontal fence 162 | //return this.decodePos(x-1, y); 163 | return null; 164 | } 165 | 166 | static click(x, y) { 167 | if (this.gameOver) { 168 | return; 169 | } 170 | const p = this.decodePos(x, y); 171 | if (p) { 172 | if (p.pawn) { 173 | if (globalInstance.exports.isPawnMoveLegal(p.pawn.x, p.pawn.y)) { 174 | globalInstance.exports.movePawn(p.pawn.x, p.pawn.y); 175 | this.fetchState(); 176 | this.drawPieces(); 177 | } 178 | } 179 | if (p.fence) { 180 | if (globalInstance.exports.isFenceMoveLegal(p.fence.x, p.fence.y, p.fence.dir)) { 181 | globalInstance.exports.moveFence(p.fence.x, p.fence.y, p.fence.dir); 182 | this.fetchState(); 183 | this.drawPieces(); 184 | } 185 | } 186 | } 187 | } 188 | 189 | static mouseOver(x, y) { 190 | if (this.gameOver) { 191 | return; 192 | } 193 | const p = this.decodePos(x, y); 194 | if (p) { 195 | if (p.pawn) { 196 | //console.log(`P ${p.pawn.x},${p.pawn.y} (${x},${y})`); 197 | if (globalInstance.exports.isPawnMoveLegal(p.pawn.x, p.pawn.y)) { 198 | this.drawPawn(this.pawns[this.pi].x, this.pawns[this.pi].y, this.colourPawnsDim[this.pi], 0, false); // old pos 199 | this.drawPawn(p.pawn.x, p.pawn.y, this.colourPawns[this.pi], this.pawnRadius, false); // new pos 200 | } else { 201 | this.drawPawn(p.pawn.x, p.pawn.y, this.colourPawnIllegal, 0, false); 202 | } 203 | } 204 | if (p.fence) { 205 | //console.log(`F ${p.fence.x},${p.fence.y},${p.fence.dir} (${x},${y})`); 206 | if (globalInstance.exports.isFenceMoveLegal(p.fence.x, p.fence.y, p.fence.dir)) { 207 | this.drawFence(p.fence.x, p.fence.y, p.fence.dir, this.colourFence); 208 | } else { 209 | this.drawFence(p.fence.x, p.fence.y, p.fence.dir, this.colourFenceIllegal); 210 | } 211 | } 212 | } 213 | } 214 | 215 | static mouseOut(x, y) { 216 | if (this.gameOver) { 217 | return; 218 | } 219 | this.drawPieces(); 220 | } 221 | 222 | static drawFence(fx, fy, dir, col) { 223 | if (!col) { 224 | return; 225 | } 226 | 227 | if (dir == this.VERT) { 228 | for (let y=0;y<5;y++) { 229 | let td = document.getElementById(`cell${fx*3+2},${fy*3+y}`); 230 | td.style['background-color'] = col; 231 | if (y == 0) { 232 | td.style['border-top-left-radius'] = this.fenceRadius; 233 | td.style['border-top-right-radius'] = this.fenceRadius; 234 | } 235 | if (y == 4) { 236 | td.style['border-bottom-left-radius'] = this.fenceRadius; 237 | td.style['border-bottom-right-radius'] = this.fenceRadius; 238 | } 239 | 240 | } 241 | } else { 242 | for (let x=0;x<5;x++) { 243 | let td = document.getElementById(`cell${fx*3+x},${fy*3+2}`); 244 | td.style['background-color'] = col; 245 | if (x == 0) { 246 | td.style['border-top-left-radius'] = this.fenceRadius; 247 | td.style['border-bottom-left-radius'] = this.fenceRadius; 248 | } 249 | if (x == 4) { 250 | td.style['border-top-right-radius'] = this.fenceRadius; 251 | td.style['border-bottom-right-radius'] = this.fenceRadius; 252 | } 253 | } 254 | } 255 | } 256 | 257 | static drawPawn(x, y, col, radius, highlight) { 258 | if (!col) { 259 | return; 260 | } 261 | if (highlight) { 262 | col = '#ffffff'; 263 | } 264 | let td = document.getElementById(`cell${x*3},${y*3}`); 265 | td.style['border-top-left-radius'] = radius; 266 | td.style['background-color'] = col; 267 | td = document.getElementById(`cell${x*3+1},${y*3}`); 268 | td.style['border-top-right-radius'] = radius; 269 | td.style['background-color'] = col; 270 | td = document.getElementById(`cell${x*3},${y*3+1}`); 271 | td.style['border-bottom-left-radius'] = radius; 272 | td.style['background-color'] = col; 273 | td = document.getElementById(`cell${x*3+1},${y*3+1}`); 274 | td.style['border-bottom-right-radius'] = radius; 275 | td.style['background-color'] = col; 276 | } 277 | 278 | static drawPieces() { 279 | const pawnSz = 2; 280 | const fenceSz = 1; 281 | const dim = pawnSz*9 + fenceSz*8; 282 | 283 | // all cells transparent 284 | for (let y=0;y { 296 | this.drawFence(f.x, f.y, f.dir, this.colourFence); 297 | }); 298 | 299 | for (let i=0;i { 423 | this.mouseOver(x, y); 424 | }; 425 | td.onmouseout = (evt) => { 426 | this.mouseOut(x, y); 427 | }; 428 | td.onclick = (evt) => { 429 | this.click(x, y); 430 | }; 431 | } 432 | } 433 | 434 | bgParentElem.appendChild(tbl); 435 | 436 | // all cells in background colour 437 | for (let y=0;y { 486 | this.mouseOver(x, y); 487 | }; 488 | td.onmouseout = (evt) => { 489 | this.mouseOut(x, y); 490 | }; 491 | td.onclick = (evt) => { 492 | this.click(x, y); 493 | }; 494 | } 495 | } 496 | 497 | parentElem.appendChild(tbl); 498 | 499 | // stats 500 | tbl = document.createElement('table'); 501 | tbl.style.width = (this.statsCellSz * this.statw) + 'px'; 502 | tbl.style.height = (this.statsCellSz * this.stath) + 'px'; 503 | tbl.style.border = '10px ridge ' + this.colourBackground; 504 | tbl.style['border-spacing'] = '0px'; 505 | tbl.style['background-color'] = 'rgba(0,0,0,0)'; 506 | 507 | for (let y=0;y