├── .github ├── FUNDING.yml └── workflows │ └── go.yml ├── LICENSE ├── README.md ├── _images ├── bulldog-down.png ├── bulldog-left.png ├── bulldog-right.png ├── bulldog-up.png ├── door.png ├── gopher-dead.png ├── gopher-down.png ├── gopher-left.png ├── gopher-right.png ├── gopher-up.png ├── marker.png ├── source.txt ├── wall.png └── won.png ├── cmd └── golab │ └── golab.go ├── docs ├── index.html ├── main.wasm └── wasm_exec.js ├── engine ├── commands.go ├── difficulty.go ├── engine.go ├── gen-lab.go ├── lab-size.go ├── model.go └── speed.go ├── go.mod ├── go.sum ├── screenshot-golab.png └── view ├── _generate-embedded-imgs └── main.go ├── embedded-imgs.go ├── images.go ├── options.go └── view.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: icza 2 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push] 3 | jobs: 4 | 5 | build: 6 | name: Build 7 | runs-on: ubuntu-latest 8 | steps: 9 | 10 | - name: Set up Go 1.13 11 | uses: actions/setup-go@v1 12 | with: 13 | go-version: 1.13 14 | id: go 15 | 16 | - name: Install required linux packages for gio 17 | run: sudo apt update && sudo apt install -y --no-install-recommends libwayland-dev libx11-dev libx11-xcb-dev libxkbcommon-x11-dev libgles2-mesa-dev libegl1-mesa-dev 18 | 19 | - name: Check out code into the Go module directory 20 | uses: actions/checkout@v2 21 | 22 | - name: Tests 23 | run: go test ./... 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Andras Belicza 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # golab 2 | 3 | [](https://github.com/icza/golab/actions) 4 | 5 | _This the reincarnation of my [gophergala/golab](https://github.com/gophergala/golab) game._ 6 | 7 | ## Introduction 8 | 9 | **Gopher's Labyrinth** (or **GoLab**) is a 2D Labyrinth game where you control Gopher 10 | (who else) and your goal is to get to the Exit point. 11 | But beware of the bloodthirsty Bulldogs, the ancient enemies of gophers who are endlessly roaming the Labyrinth! 12 | 13 | Controlling Gopher is very easy: just click with your left mouse button to where you want to move 14 | (there must be a free straight line to it). You may queue multiple target points forming a path. 15 | Right click clears the path. You may also use the arrow keys on your keyboard. 16 | 17 | You may try out the game in your browser if it supports WebAssembly and WebGL here: https://icza.github.io/golab/ 18 | 19 |  20 | 21 | ## Under the hood 22 | 23 | GoLab is written completely in [Go](https://golang.org). Go 1.13 or newer is required. 24 | The user interface and input handling is done with the [gioui](https://gioui.org) library, utilized in the `view` package. 25 | 26 | The game model and game logic is placed in the `engine` package. 27 | 28 | ## How to get it or install it 29 | 30 | Of course in the "go way". You may quickly test it by initializing a new module in a folder by running: 31 | 32 | go mod init test 33 | 34 | And then run GoLab with: 35 | 36 | go run github.com/icza/golab/cmd/golab 37 | 38 | Or try it in your browser: https://icza.github.io/golab/ 39 | 40 | ## LICENSE 41 | 42 | See [LICENSE](https://github.com/icza/golab/blob/master/LICENSE). 43 | 44 | GoLab's Gopher is a derivative work based on the Go gopher which was designed by Renee French. (http://reneefrench.blogspot.com/). 45 | Licensed under the Creative Commons 3.0 Attributions license. 46 | 47 | The source of other images can be found in the [\_images_/source.txt](https://github.com/icza/golab/blob/master/_images/source.txt) file. 48 | -------------------------------------------------------------------------------- /_images/bulldog-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icza/golab/59ee81c619e56dbe1302f3471d639ecde0e2c512/_images/bulldog-down.png -------------------------------------------------------------------------------- /_images/bulldog-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icza/golab/59ee81c619e56dbe1302f3471d639ecde0e2c512/_images/bulldog-left.png -------------------------------------------------------------------------------- /_images/bulldog-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icza/golab/59ee81c619e56dbe1302f3471d639ecde0e2c512/_images/bulldog-right.png -------------------------------------------------------------------------------- /_images/bulldog-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icza/golab/59ee81c619e56dbe1302f3471d639ecde0e2c512/_images/bulldog-up.png -------------------------------------------------------------------------------- /_images/door.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icza/golab/59ee81c619e56dbe1302f3471d639ecde0e2c512/_images/door.png -------------------------------------------------------------------------------- /_images/gopher-dead.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icza/golab/59ee81c619e56dbe1302f3471d639ecde0e2c512/_images/gopher-dead.png -------------------------------------------------------------------------------- /_images/gopher-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icza/golab/59ee81c619e56dbe1302f3471d639ecde0e2c512/_images/gopher-down.png -------------------------------------------------------------------------------- /_images/gopher-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icza/golab/59ee81c619e56dbe1302f3471d639ecde0e2c512/_images/gopher-left.png -------------------------------------------------------------------------------- /_images/gopher-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icza/golab/59ee81c619e56dbe1302f3471d639ecde0e2c512/_images/gopher-right.png -------------------------------------------------------------------------------- /_images/gopher-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icza/golab/59ee81c619e56dbe1302f3471d639ecde0e2c512/_images/gopher-up.png -------------------------------------------------------------------------------- /_images/marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icza/golab/59ee81c619e56dbe1302f3471d639ecde0e2c512/_images/marker.png -------------------------------------------------------------------------------- /_images/source.txt: -------------------------------------------------------------------------------- 1 | These resources are not required at runtime, they are embedded into the executable binary. 2 | Whether the embedded images are used is controlled by the useEmbeddedImages constant/variable in view/images.go file. 3 | If it is set to false, these images are searched for in the current directory. 4 | 5 | 6 | Original Gopher image from: 7 | https://github.com/golang-samples/gopher-vector 8 | 9 | Original Bulldog images from: 10 | http://www.3dcadbrowser.com/download.aspx?3dmodel=14738 11 | 12 | Wall image from: 13 | http://gamedesign.wdfiles.com/local--files/spriteart%3Asprite-art-101-basic-depth/sprite_art_depth_tutorial_1.png 14 | 15 | Target image (marker): 16 | https://www.iconfinder.com/icons/73052/base_biswajit_chartreuse_con_map_marker_outside_pixe_icon 17 | 18 | Door (Exit sign) image from: 19 | http://www.fengshuiatwork.com/blog/wp-content/uploads/2011/09/front-door.png 20 | 21 | Congratulations image from: 22 | http://www.wetumpkaband.com/images/Congratulations.png 23 | 24 | -------------------------------------------------------------------------------- /_images/wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icza/golab/59ee81c619e56dbe1302f3471d639ecde0e2c512/_images/wall.png -------------------------------------------------------------------------------- /_images/won.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icza/golab/59ee81c619e56dbe1302f3471d639ecde0e2c512/_images/won.png -------------------------------------------------------------------------------- /cmd/golab/golab.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "gioui.org/app" 5 | "gioui.org/unit" 6 | "github.com/icza/golab/engine" 7 | "github.com/icza/golab/view" 8 | ) 9 | 10 | func main() { 11 | go func() { 12 | w := app.NewWindow( 13 | app.Title("Gopher's Labyrinth"), 14 | app.Size(unit.Px(view.WindowWidthPx), unit.Px(view.WindowHeightPx)), 15 | ) 16 | 17 | eng := engine.NewEngine(w.Invalidate) 18 | go eng.Loop() 19 | 20 | v := view.New(eng, w) 21 | v.Loop() 22 | }() 23 | 24 | app.Main() 25 | } 26 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 21 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /docs/main.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icza/golab/59ee81c619e56dbe1302f3471d639ecde0e2c512/docs/main.wasm -------------------------------------------------------------------------------- /docs/wasm_exec.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | (() => { 6 | // Map multiple JavaScript environments to a single common API, 7 | // preferring web standards over Node.js API. 8 | // 9 | // Environments considered: 10 | // - Browsers 11 | // - Node.js 12 | // - Electron 13 | // - Parcel 14 | 15 | if (typeof global !== "undefined") { 16 | // global already exists 17 | } else if (typeof window !== "undefined") { 18 | window.global = window; 19 | } else if (typeof self !== "undefined") { 20 | self.global = self; 21 | } else { 22 | throw new Error("cannot export Go (neither global, window nor self is defined)"); 23 | } 24 | 25 | if (!global.require && typeof require !== "undefined") { 26 | global.require = require; 27 | } 28 | 29 | if (!global.fs && global.require) { 30 | global.fs = require("fs"); 31 | } 32 | 33 | const enosys = () => { 34 | const err = new Error("not implemented"); 35 | err.code = "ENOSYS"; 36 | return err; 37 | }; 38 | 39 | if (!global.fs) { 40 | let outputBuf = ""; 41 | global.fs = { 42 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused 43 | writeSync(fd, buf) { 44 | outputBuf += decoder.decode(buf); 45 | const nl = outputBuf.lastIndexOf("\n"); 46 | if (nl != -1) { 47 | console.log(outputBuf.substr(0, nl)); 48 | outputBuf = outputBuf.substr(nl + 1); 49 | } 50 | return buf.length; 51 | }, 52 | write(fd, buf, offset, length, position, callback) { 53 | if (offset !== 0 || length !== buf.length || position !== null) { 54 | callback(enosys()); 55 | return; 56 | } 57 | const n = this.writeSync(fd, buf); 58 | callback(null, n); 59 | }, 60 | chmod(path, mode, callback) { callback(enosys()); }, 61 | chown(path, uid, gid, callback) { callback(enosys()); }, 62 | close(fd, callback) { callback(enosys()); }, 63 | fchmod(fd, mode, callback) { callback(enosys()); }, 64 | fchown(fd, uid, gid, callback) { callback(enosys()); }, 65 | fstat(fd, callback) { callback(enosys()); }, 66 | fsync(fd, callback) { callback(null); }, 67 | ftruncate(fd, length, callback) { callback(enosys()); }, 68 | lchown(path, uid, gid, callback) { callback(enosys()); }, 69 | link(path, link, callback) { callback(enosys()); }, 70 | lstat(path, callback) { callback(enosys()); }, 71 | mkdir(path, perm, callback) { callback(enosys()); }, 72 | open(path, flags, mode, callback) { callback(enosys()); }, 73 | read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, 74 | readdir(path, callback) { callback(enosys()); }, 75 | readlink(path, callback) { callback(enosys()); }, 76 | rename(from, to, callback) { callback(enosys()); }, 77 | rmdir(path, callback) { callback(enosys()); }, 78 | stat(path, callback) { callback(enosys()); }, 79 | symlink(path, link, callback) { callback(enosys()); }, 80 | truncate(path, length, callback) { callback(enosys()); }, 81 | unlink(path, callback) { callback(enosys()); }, 82 | utimes(path, atime, mtime, callback) { callback(enosys()); }, 83 | }; 84 | } 85 | 86 | if (!global.process) { 87 | global.process = { 88 | getuid() { return -1; }, 89 | getgid() { return -1; }, 90 | geteuid() { return -1; }, 91 | getegid() { return -1; }, 92 | getgroups() { throw enosys(); }, 93 | pid: -1, 94 | ppid: -1, 95 | umask() { throw enosys(); }, 96 | cwd() { throw enosys(); }, 97 | chdir() { throw enosys(); }, 98 | } 99 | } 100 | 101 | if (!global.crypto) { 102 | const nodeCrypto = require("crypto"); 103 | global.crypto = { 104 | getRandomValues(b) { 105 | nodeCrypto.randomFillSync(b); 106 | }, 107 | }; 108 | } 109 | 110 | if (!global.performance) { 111 | global.performance = { 112 | now() { 113 | const [sec, nsec] = process.hrtime(); 114 | return sec * 1000 + nsec / 1000000; 115 | }, 116 | }; 117 | } 118 | 119 | if (!global.TextEncoder) { 120 | global.TextEncoder = require("util").TextEncoder; 121 | } 122 | 123 | if (!global.TextDecoder) { 124 | global.TextDecoder = require("util").TextDecoder; 125 | } 126 | 127 | // End of polyfills for common API. 128 | 129 | const encoder = new TextEncoder("utf-8"); 130 | const decoder = new TextDecoder("utf-8"); 131 | 132 | global.Go = class { 133 | constructor() { 134 | this.argv = ["js"]; 135 | this.env = {}; 136 | this.exit = (code) => { 137 | if (code !== 0) { 138 | console.warn("exit code:", code); 139 | } 140 | }; 141 | this._exitPromise = new Promise((resolve) => { 142 | this._resolveExitPromise = resolve; 143 | }); 144 | this._pendingEvent = null; 145 | this._scheduledTimeouts = new Map(); 146 | this._nextCallbackTimeoutID = 1; 147 | 148 | const setInt64 = (addr, v) => { 149 | this.mem.setUint32(addr + 0, v, true); 150 | this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); 151 | } 152 | 153 | const getInt64 = (addr) => { 154 | const low = this.mem.getUint32(addr + 0, true); 155 | const high = this.mem.getInt32(addr + 4, true); 156 | return low + high * 4294967296; 157 | } 158 | 159 | const loadValue = (addr) => { 160 | const f = this.mem.getFloat64(addr, true); 161 | if (f === 0) { 162 | return undefined; 163 | } 164 | if (!isNaN(f)) { 165 | return f; 166 | } 167 | 168 | const id = this.mem.getUint32(addr, true); 169 | return this._values[id]; 170 | } 171 | 172 | const storeValue = (addr, v) => { 173 | const nanHead = 0x7FF80000; 174 | 175 | if (typeof v === "number") { 176 | if (isNaN(v)) { 177 | this.mem.setUint32(addr + 4, nanHead, true); 178 | this.mem.setUint32(addr, 0, true); 179 | return; 180 | } 181 | if (v === 0) { 182 | this.mem.setUint32(addr + 4, nanHead, true); 183 | this.mem.setUint32(addr, 1, true); 184 | return; 185 | } 186 | this.mem.setFloat64(addr, v, true); 187 | return; 188 | } 189 | 190 | switch (v) { 191 | case undefined: 192 | this.mem.setFloat64(addr, 0, true); 193 | return; 194 | case null: 195 | this.mem.setUint32(addr + 4, nanHead, true); 196 | this.mem.setUint32(addr, 2, true); 197 | return; 198 | case true: 199 | this.mem.setUint32(addr + 4, nanHead, true); 200 | this.mem.setUint32(addr, 3, true); 201 | return; 202 | case false: 203 | this.mem.setUint32(addr + 4, nanHead, true); 204 | this.mem.setUint32(addr, 4, true); 205 | return; 206 | } 207 | 208 | let id = this._ids.get(v); 209 | if (id === undefined) { 210 | id = this._idPool.pop(); 211 | if (id === undefined) { 212 | id = this._values.length; 213 | } 214 | this._values[id] = v; 215 | this._goRefCounts[id] = 0; 216 | this._ids.set(v, id); 217 | } 218 | this._goRefCounts[id]++; 219 | let typeFlag = 1; 220 | switch (typeof v) { 221 | case "string": 222 | typeFlag = 2; 223 | break; 224 | case "symbol": 225 | typeFlag = 3; 226 | break; 227 | case "function": 228 | typeFlag = 4; 229 | break; 230 | } 231 | this.mem.setUint32(addr + 4, nanHead | typeFlag, true); 232 | this.mem.setUint32(addr, id, true); 233 | } 234 | 235 | const loadSlice = (addr) => { 236 | const array = getInt64(addr + 0); 237 | const len = getInt64(addr + 8); 238 | return new Uint8Array(this._inst.exports.mem.buffer, array, len); 239 | } 240 | 241 | const loadSliceOfValues = (addr) => { 242 | const array = getInt64(addr + 0); 243 | const len = getInt64(addr + 8); 244 | const a = new Array(len); 245 | for (let i = 0; i < len; i++) { 246 | a[i] = loadValue(array + i * 8); 247 | } 248 | return a; 249 | } 250 | 251 | const loadString = (addr) => { 252 | const saddr = getInt64(addr + 0); 253 | const len = getInt64(addr + 8); 254 | return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); 255 | } 256 | 257 | const timeOrigin = Date.now() - performance.now(); 258 | this.importObject = { 259 | go: { 260 | // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) 261 | // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported 262 | // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). 263 | // This changes the SP, thus we have to update the SP used by the imported function. 264 | 265 | // func wasmExit(code int32) 266 | "runtime.wasmExit": (sp) => { 267 | const code = this.mem.getInt32(sp + 8, true); 268 | this.exited = true; 269 | delete this._inst; 270 | delete this._values; 271 | delete this._goRefCounts; 272 | delete this._ids; 273 | delete this._idPool; 274 | this.exit(code); 275 | }, 276 | 277 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) 278 | "runtime.wasmWrite": (sp) => { 279 | const fd = getInt64(sp + 8); 280 | const p = getInt64(sp + 16); 281 | const n = this.mem.getInt32(sp + 24, true); 282 | fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); 283 | }, 284 | 285 | // func resetMemoryDataView() 286 | "runtime.resetMemoryDataView": (sp) => { 287 | this.mem = new DataView(this._inst.exports.mem.buffer); 288 | }, 289 | 290 | // func nanotime1() int64 291 | "runtime.nanotime1": (sp) => { 292 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); 293 | }, 294 | 295 | // func walltime1() (sec int64, nsec int32) 296 | "runtime.walltime1": (sp) => { 297 | const msec = (new Date).getTime(); 298 | setInt64(sp + 8, msec / 1000); 299 | this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); 300 | }, 301 | 302 | // func scheduleTimeoutEvent(delay int64) int32 303 | "runtime.scheduleTimeoutEvent": (sp) => { 304 | const id = this._nextCallbackTimeoutID; 305 | this._nextCallbackTimeoutID++; 306 | this._scheduledTimeouts.set(id, setTimeout( 307 | () => { 308 | this._resume(); 309 | while (this._scheduledTimeouts.has(id)) { 310 | // for some reason Go failed to register the timeout event, log and try again 311 | // (temporary workaround for https://github.com/golang/go/issues/28975) 312 | console.warn("scheduleTimeoutEvent: missed timeout event"); 313 | this._resume(); 314 | } 315 | }, 316 | getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early 317 | )); 318 | this.mem.setInt32(sp + 16, id, true); 319 | }, 320 | 321 | // func clearTimeoutEvent(id int32) 322 | "runtime.clearTimeoutEvent": (sp) => { 323 | const id = this.mem.getInt32(sp + 8, true); 324 | clearTimeout(this._scheduledTimeouts.get(id)); 325 | this._scheduledTimeouts.delete(id); 326 | }, 327 | 328 | // func getRandomData(r []byte) 329 | "runtime.getRandomData": (sp) => { 330 | crypto.getRandomValues(loadSlice(sp + 8)); 331 | }, 332 | 333 | // func finalizeRef(v ref) 334 | "syscall/js.finalizeRef": (sp) => { 335 | const id = this.mem.getUint32(sp + 8, true); 336 | this._goRefCounts[id]--; 337 | if (this._goRefCounts[id] === 0) { 338 | const v = this._values[id]; 339 | this._values[id] = null; 340 | this._ids.delete(v); 341 | this._idPool.push(id); 342 | } 343 | }, 344 | 345 | // func stringVal(value string) ref 346 | "syscall/js.stringVal": (sp) => { 347 | storeValue(sp + 24, loadString(sp + 8)); 348 | }, 349 | 350 | // func valueGet(v ref, p string) ref 351 | "syscall/js.valueGet": (sp) => { 352 | const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); 353 | sp = this._inst.exports.getsp(); // see comment above 354 | storeValue(sp + 32, result); 355 | }, 356 | 357 | // func valueSet(v ref, p string, x ref) 358 | "syscall/js.valueSet": (sp) => { 359 | Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); 360 | }, 361 | 362 | // func valueDelete(v ref, p string) 363 | "syscall/js.valueDelete": (sp) => { 364 | Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); 365 | }, 366 | 367 | // func valueIndex(v ref, i int) ref 368 | "syscall/js.valueIndex": (sp) => { 369 | storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); 370 | }, 371 | 372 | // valueSetIndex(v ref, i int, x ref) 373 | "syscall/js.valueSetIndex": (sp) => { 374 | Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); 375 | }, 376 | 377 | // func valueCall(v ref, m string, args []ref) (ref, bool) 378 | "syscall/js.valueCall": (sp) => { 379 | try { 380 | const v = loadValue(sp + 8); 381 | const m = Reflect.get(v, loadString(sp + 16)); 382 | const args = loadSliceOfValues(sp + 32); 383 | const result = Reflect.apply(m, v, args); 384 | sp = this._inst.exports.getsp(); // see comment above 385 | storeValue(sp + 56, result); 386 | this.mem.setUint8(sp + 64, 1); 387 | } catch (err) { 388 | storeValue(sp + 56, err); 389 | this.mem.setUint8(sp + 64, 0); 390 | } 391 | }, 392 | 393 | // func valueInvoke(v ref, args []ref) (ref, bool) 394 | "syscall/js.valueInvoke": (sp) => { 395 | try { 396 | const v = loadValue(sp + 8); 397 | const args = loadSliceOfValues(sp + 16); 398 | const result = Reflect.apply(v, undefined, args); 399 | sp = this._inst.exports.getsp(); // see comment above 400 | storeValue(sp + 40, result); 401 | this.mem.setUint8(sp + 48, 1); 402 | } catch (err) { 403 | storeValue(sp + 40, err); 404 | this.mem.setUint8(sp + 48, 0); 405 | } 406 | }, 407 | 408 | // func valueNew(v ref, args []ref) (ref, bool) 409 | "syscall/js.valueNew": (sp) => { 410 | try { 411 | const v = loadValue(sp + 8); 412 | const args = loadSliceOfValues(sp + 16); 413 | const result = Reflect.construct(v, args); 414 | sp = this._inst.exports.getsp(); // see comment above 415 | storeValue(sp + 40, result); 416 | this.mem.setUint8(sp + 48, 1); 417 | } catch (err) { 418 | storeValue(sp + 40, err); 419 | this.mem.setUint8(sp + 48, 0); 420 | } 421 | }, 422 | 423 | // func valueLength(v ref) int 424 | "syscall/js.valueLength": (sp) => { 425 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); 426 | }, 427 | 428 | // valuePrepareString(v ref) (ref, int) 429 | "syscall/js.valuePrepareString": (sp) => { 430 | const str = encoder.encode(String(loadValue(sp + 8))); 431 | storeValue(sp + 16, str); 432 | setInt64(sp + 24, str.length); 433 | }, 434 | 435 | // valueLoadString(v ref, b []byte) 436 | "syscall/js.valueLoadString": (sp) => { 437 | const str = loadValue(sp + 8); 438 | loadSlice(sp + 16).set(str); 439 | }, 440 | 441 | // func valueInstanceOf(v ref, t ref) bool 442 | "syscall/js.valueInstanceOf": (sp) => { 443 | this.mem.setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16)); 444 | }, 445 | 446 | // func copyBytesToGo(dst []byte, src ref) (int, bool) 447 | "syscall/js.copyBytesToGo": (sp) => { 448 | const dst = loadSlice(sp + 8); 449 | const src = loadValue(sp + 32); 450 | if (!(src instanceof Uint8Array)) { 451 | this.mem.setUint8(sp + 48, 0); 452 | return; 453 | } 454 | const toCopy = src.subarray(0, dst.length); 455 | dst.set(toCopy); 456 | setInt64(sp + 40, toCopy.length); 457 | this.mem.setUint8(sp + 48, 1); 458 | }, 459 | 460 | // func copyBytesToJS(dst ref, src []byte) (int, bool) 461 | "syscall/js.copyBytesToJS": (sp) => { 462 | const dst = loadValue(sp + 8); 463 | const src = loadSlice(sp + 16); 464 | if (!(dst instanceof Uint8Array)) { 465 | this.mem.setUint8(sp + 48, 0); 466 | return; 467 | } 468 | const toCopy = src.subarray(0, dst.length); 469 | dst.set(toCopy); 470 | setInt64(sp + 40, toCopy.length); 471 | this.mem.setUint8(sp + 48, 1); 472 | }, 473 | 474 | "debug": (value) => { 475 | console.log(value); 476 | }, 477 | } 478 | }; 479 | } 480 | 481 | async run(instance) { 482 | this._inst = instance; 483 | this.mem = new DataView(this._inst.exports.mem.buffer); 484 | this._values = [ // JS values that Go currently has references to, indexed by reference id 485 | NaN, 486 | 0, 487 | null, 488 | true, 489 | false, 490 | global, 491 | this, 492 | ]; 493 | this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id 494 | this._ids = new Map(); // mapping from JS values to reference ids 495 | this._idPool = []; // unused ids that have been garbage collected 496 | this.exited = false; // whether the Go program has exited 497 | 498 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. 499 | let offset = 4096; 500 | 501 | const strPtr = (str) => { 502 | const ptr = offset; 503 | const bytes = encoder.encode(str + "\0"); 504 | new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); 505 | offset += bytes.length; 506 | if (offset % 8 !== 0) { 507 | offset += 8 - (offset % 8); 508 | } 509 | return ptr; 510 | }; 511 | 512 | const argc = this.argv.length; 513 | 514 | const argvPtrs = []; 515 | this.argv.forEach((arg) => { 516 | argvPtrs.push(strPtr(arg)); 517 | }); 518 | argvPtrs.push(0); 519 | 520 | const keys = Object.keys(this.env).sort(); 521 | keys.forEach((key) => { 522 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); 523 | }); 524 | argvPtrs.push(0); 525 | 526 | const argv = offset; 527 | argvPtrs.forEach((ptr) => { 528 | this.mem.setUint32(offset, ptr, true); 529 | this.mem.setUint32(offset + 4, 0, true); 530 | offset += 8; 531 | }); 532 | 533 | this._inst.exports.run(argc, argv); 534 | if (this.exited) { 535 | this._resolveExitPromise(); 536 | } 537 | await this._exitPromise; 538 | } 539 | 540 | _resume() { 541 | if (this.exited) { 542 | throw new Error("Go program has already exited"); 543 | } 544 | this._inst.exports.resume(); 545 | if (this.exited) { 546 | this._resolveExitPromise(); 547 | } 548 | } 549 | 550 | _makeFuncWrapper(id) { 551 | const go = this; 552 | return function () { 553 | const event = { id: id, this: this, args: arguments }; 554 | go._pendingEvent = event; 555 | go._resume(); 556 | return event.result; 557 | }; 558 | } 559 | } 560 | 561 | if ( 562 | global.require && 563 | global.require.main === module && 564 | global.process && 565 | global.process.versions && 566 | !global.process.versions.electron 567 | ) { 568 | if (process.argv.length < 3) { 569 | console.error("usage: go_js_wasm_exec [wasm binary] [arguments]"); 570 | process.exit(1); 571 | } 572 | 573 | const go = new Go(); 574 | go.argv = process.argv.slice(2); 575 | go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env); 576 | go.exit = process.exit; 577 | WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { 578 | process.on("exit", (code) => { // Node.js exits if no event handler is pending 579 | if (code === 0 && !go.exited) { 580 | // deadlock, make Go print error and stack traces 581 | go._pendingEvent = { id: 0 }; 582 | go._resume(); 583 | } 584 | }); 585 | return go.run(result.instance); 586 | }).catch((err) => { 587 | console.error(err); 588 | process.exit(1); 589 | }); 590 | } 591 | })(); 592 | -------------------------------------------------------------------------------- /engine/commands.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | // GameConfig holds config to start a new game. 4 | type GameConfig struct { 5 | Difficulty *Difficulty 6 | LabSize *LabSize 7 | Speed *Speed 8 | } 9 | 10 | // Click describes a click event. 11 | type Click struct { 12 | X, Y int // Click coordinates in the lab 13 | Left bool // Tells if left button was pressed 14 | Right bool // Tells if right button was pressed 15 | } 16 | 17 | // Key describes a key event. 18 | type Key struct { 19 | DirKeys map[Dir]bool // Tells if keys for the directions were pressed 20 | } 21 | -------------------------------------------------------------------------------- /engine/difficulty.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | // Difficulty of the game. 4 | type Difficulty struct { 5 | Name string 6 | 7 | // "Bulldog density", it tells how many Bulldogs to generate for an area of 1,000 blocks. 8 | // For example if this is 10.0 and rows*cols = 21*21 = 441, 10.0*441/1000 = 4.41 => 4 Bulldogs will be generated. 9 | bulldogDensity float64 10 | 11 | Default bool 12 | } 13 | 14 | func (d *Difficulty) String() string { 15 | return d.Name 16 | } 17 | 18 | // Difficulties is a slice of all, ordered difficulties. 19 | var Difficulties = []*Difficulty{ 20 | &Difficulty{Name: "Baby", bulldogDensity: 0}, 21 | &Difficulty{Name: "Easy", bulldogDensity: 5}, 22 | &Difficulty{Name: "Normal", bulldogDensity: 10, Default: true}, 23 | &Difficulty{Name: "Hard", bulldogDensity: 20}, 24 | &Difficulty{Name: "Brutal", bulldogDensity: 40}, 25 | } 26 | 27 | // DifficultyDefaultIdx is the index of the default difficulty in Difficulties. 28 | var DifficultyDefaultIdx int 29 | 30 | func init() { 31 | for i, d := range Difficulties { 32 | if d.Default { 33 | DifficultyDefaultIdx = i 34 | break 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /engine/engine.go: -------------------------------------------------------------------------------- 1 | // Package engine is the game engien: it contains the game model and game logic. 2 | // 3 | // The engine's Loop() method should be launched as a goroutine, 4 | // and it can be controlled with opaque commands safely from other 5 | // goroutines. 6 | package engine 7 | 8 | import ( 9 | "image" 10 | "log" 11 | "math" 12 | "math/rand" 13 | "time" 14 | ) 15 | 16 | const ( 17 | // BlockSize is the size of the labyrinth unit in pixels. 18 | BlockSize = 40 19 | ) 20 | 21 | func init() { 22 | rand.Seed(time.Now().UnixNano()) 23 | } 24 | 25 | var ( 26 | // dt is the delta time between iterations. 27 | // We keep this fixed to simulate slower / faster game speeds. 28 | dt = (50 * time.Millisecond).Seconds() 29 | 30 | // v is the moving speed of Gopher and the Buddlogs in pixel/sec. 31 | v = 2.0 * BlockSize 32 | ) 33 | 34 | // Engine calculates and controls the game. 35 | type Engine struct { 36 | Model *Model 37 | 38 | // command channel to control the engine from other goroutines. 39 | cmdChan chan interface{} 40 | 41 | // invalidate is called by the engine to request a new view frame. 42 | invalidate func() 43 | 44 | // Current game config 45 | cfg *GameConfig 46 | 47 | // directions is a reused slice of all directions 48 | directions []Dir 49 | } 50 | 51 | // NewEngine returns a new Engine. 52 | // invalidate is a func which will be called by the engine to request a new view frame. 53 | func NewEngine(invalidate func()) *Engine { 54 | e := &Engine{ 55 | Model: &Model{ 56 | TargetPoss: make([]image.Point, 0, 20), // cap defines max queueable points 57 | }, 58 | cmdChan: make(chan interface{}, 10), 59 | invalidate: invalidate, 60 | directions: make([]Dir, DirCount), 61 | } 62 | 63 | // Populate the directions slice 64 | for i := range e.directions { 65 | e.directions[i] = Dir(i) 66 | } 67 | 68 | e.initNewGame(&GameConfig{ 69 | Difficulty: Difficulties[DifficultyDefaultIdx], 70 | LabSize: LabSizes[LabSizeDefaultIdx], 71 | Speed: Speeds[SpeedDefaultIdx], 72 | }) 73 | 74 | return e 75 | } 76 | 77 | // NewGame enqueues a new game command with the given config. 78 | func (e *Engine) NewGame(cfg GameConfig) { 79 | e.cmdChan <- &cfg 80 | } 81 | 82 | // SendClick sends a click event from the user. 83 | func (e *Engine) SendClick(c Click) { 84 | e.cmdChan <- &c 85 | } 86 | 87 | // SendKey sends a key event from the user. 88 | func (e *Engine) SendKey(k Key) { 89 | e.cmdChan <- &k 90 | } 91 | 92 | // Loop starts calculating the game. 93 | // This function returns only if the user closes the app. 94 | func (e *Engine) Loop() { 95 | for { 96 | e.Model.Lock() 97 | 98 | e.processCmds() 99 | 100 | if !e.Model.Won { 101 | e.stepGopher() 102 | e.stepBulldogs() 103 | } 104 | 105 | e.Model.Unlock() 106 | 107 | e.invalidate() 108 | 109 | time.Sleep(e.cfg.Speed.loopDelay) 110 | } 111 | } 112 | 113 | // processCmds processes queued commands. 114 | func (e *Engine) processCmds() { 115 | for { 116 | select { 117 | 118 | case cmd := <-e.cmdChan: 119 | switch cmd := cmd.(type) { 120 | case *GameConfig: 121 | e.initNewGame(cmd) 122 | case *Click: 123 | e.handleClick(cmd) 124 | case *Key: 125 | e.handleKey(cmd) 126 | default: 127 | log.Printf("Unhandled cmd type: %T", cmd) 128 | } 129 | 130 | default: 131 | return // No more commands queued 132 | } 133 | } 134 | } 135 | 136 | // handleClick handles a Click command 137 | func (e *Engine) handleClick(c *Click) { 138 | m := e.Model 139 | 140 | if m.Dead || m.Won { 141 | return 142 | } 143 | 144 | if c.Right { 145 | m.TargetPoss = m.TargetPoss[:0] 146 | // Also change Gopher's current target to remain on current block: 147 | m.Gopher.TargetPos.X = int(m.Gopher.Pos.X)/BlockSize*BlockSize + BlockSize/2 148 | m.Gopher.TargetPos.Y = int(m.Gopher.Pos.Y)/BlockSize*BlockSize + BlockSize/2 149 | return 150 | } 151 | 152 | // If target buffer is full, do nothing: 153 | if len(m.TargetPoss) == cap(m.TargetPoss) { 154 | return 155 | } 156 | 157 | // Last target pos: 158 | var TargetPos image.Point 159 | if len(m.TargetPoss) == 0 { 160 | TargetPos = m.Gopher.TargetPos 161 | } else { 162 | TargetPos = m.TargetPoss[len(m.TargetPoss)-1] 163 | } 164 | 165 | // Check if new desired target is in the same row/column as the last target and if there is a free passage to there. 166 | pCol, pRow := TargetPos.X/BlockSize, TargetPos.Y/BlockSize 167 | tCol, tRow := c.X/BlockSize, c.Y/BlockSize 168 | 169 | // sorted simply returns its parameters in ascendant order: 170 | sorted := func(a, b int) (int, int) { 171 | if a < b { 172 | return a, b 173 | } 174 | return b, a 175 | } 176 | 177 | if pCol == tCol { // Same column 178 | for row, row2 := sorted(pRow, tRow); row <= row2; row++ { 179 | if m.Lab[row][tCol] == BlockWall { 180 | return // Wall in the route 181 | } 182 | } 183 | } else if pRow == tRow { // Same row 184 | for col, col2 := sorted(pCol, tCol); col <= col2; col++ { 185 | if m.Lab[tRow][col] == BlockWall { 186 | return // Wall in the route 187 | } 188 | } 189 | } else { 190 | return // Only the same row or column can be commanded 191 | } 192 | 193 | // Target pos is allowed and reachable. 194 | // Use target position rounded to the center of the target block: 195 | m.TargetPoss = append(m.TargetPoss, image.Pt(tCol*BlockSize+BlockSize/2, tRow*BlockSize+BlockSize/2)) 196 | } 197 | 198 | // handleKey handles a Key command. 199 | func (e *Engine) handleKey(k *Key) { 200 | m := e.Model 201 | 202 | if m.Dead || m.Won { 203 | return 204 | } 205 | 206 | Gopher := m.Gopher 207 | 208 | for dir := Dir(0); dir < DirCount; dir++ { 209 | if !k.DirKeys[dir] { 210 | continue 211 | } 212 | Gopher.Dir = dir 213 | // If Gopher's target is more than a block away, clear that target: 214 | dx, dy := Gopher.TargetPos.X-int(Gopher.Pos.X), Gopher.TargetPos.Y-int(Gopher.Pos.Y) 215 | if dx <= -BlockSize || dx >= BlockSize || dy <= -BlockSize || dy >= BlockSize { 216 | m.TargetPoss = m.TargetPoss[:0] 217 | m.Gopher.TargetPos.X = int(m.Gopher.Pos.X)/BlockSize*BlockSize + BlockSize/2 218 | m.Gopher.TargetPos.Y = int(m.Gopher.Pos.Y)/BlockSize*BlockSize + BlockSize/2 219 | } 220 | 221 | col, row := Gopher.TargetPos.X/BlockSize, Gopher.TargetPos.Y/BlockSize 222 | 223 | var drow, dcol int 224 | switch dir { 225 | case DirLeft: 226 | dcol = -1 227 | case DirRight: 228 | dcol = 1 229 | case DirUp: 230 | drow = -1 231 | case DirDown: 232 | drow = 1 233 | } 234 | 235 | // If current target is in the opposite direction, then 236 | // use the new target as the current (overwriting old). 237 | // Else just queue the new target. 238 | if dx*dcol < 0 || dy*drow < 0 { 239 | m.TargetPoss = m.TargetPoss[:0] 240 | Gopher.TargetPos.X = (col+dcol)*BlockSize + BlockSize/2 241 | Gopher.TargetPos.Y = (row+drow)*BlockSize + BlockSize/2 242 | } else if m.Lab[row+drow][col+dcol] == BlockEmpty { 243 | m.TargetPoss = m.TargetPoss[:0] 244 | m.TargetPoss = append(m.TargetPoss, image.Point{ 245 | X: (col+dcol)*BlockSize + BlockSize/2, 246 | Y: (row+drow)*BlockSize + BlockSize/2}, 247 | ) 248 | break 249 | } 250 | } 251 | } 252 | 253 | // initNewGame handles a GameConfig command: initializes a new game. 254 | func (e *Engine) initNewGame(cfg *GameConfig) { 255 | e.cfg = cfg 256 | 257 | m := e.Model 258 | 259 | m.Counter++ 260 | 261 | // Init the labyrinth 262 | m.Rows, m.Cols = cfg.LabSize.rows, cfg.LabSize.cols 263 | m.Lab = make([][]Block, m.Rows) 264 | for row := range m.Lab { 265 | m.Lab[row] = make([]Block, m.Cols) 266 | } 267 | generateLab(m.Lab) 268 | 269 | m.ExitPos.X, m.ExitPos.Y = (m.Cols-2)*BlockSize+BlockSize/2, (m.Rows-2)*BlockSize+BlockSize/2 270 | 271 | // Init Gopher 272 | m.Gopher = new(MovingObj) 273 | m.Gopher.Pos.X = BlockSize + BlockSize/2 // Position Gopher to top left corner 274 | m.Gopher.Pos.Y = m.Gopher.Pos.X 275 | m.Gopher.Dir = DirRight 276 | m.Gopher.TargetPos.X = int(m.Gopher.Pos.X) 277 | m.Gopher.TargetPos.Y = int(m.Gopher.Pos.Y) 278 | 279 | // Init bulldogs 280 | numBulldogs := int(float64(m.Rows*m.Cols) * cfg.Difficulty.bulldogDensity / 1000) 281 | m.Bulldogs = make([]*MovingObj, numBulldogs) 282 | for i := range m.Bulldogs { 283 | bd := new(MovingObj) 284 | m.Bulldogs[i] = bd 285 | 286 | // Place bulldog at a random position 287 | var row, col = int(m.Gopher.Pos.Y) / BlockSize, int(m.Gopher.Pos.X) / BlockSize 288 | // Give some space to Gopher: do not generate Bulldogs too close: 289 | for gr, gc := row, col; (row-gr)*(row-gr) <= 16 && (col-gc)*(col-gc) <= 16; row, col = rPassPos(0, m.Rows), rPassPos(0, m.Cols) { 290 | } 291 | 292 | bd.Pos.X = float64(col*BlockSize + BlockSize/2) 293 | bd.Pos.Y = float64(row*BlockSize + BlockSize/2) 294 | 295 | bd.TargetPos.X, bd.TargetPos.Y = int(bd.Pos.X), int(bd.Pos.Y) 296 | } 297 | 298 | m.Dead = false 299 | m.Won = false 300 | 301 | // Throw away queued targets 302 | m.TargetPoss = m.TargetPoss[:0] 303 | } 304 | 305 | // stepGopher handles moving the Gopher and also handles the multiple target positions of Gopher. 306 | func (e *Engine) stepGopher() { 307 | m := e.Model 308 | Gopher := m.Gopher 309 | 310 | if m.Dead { 311 | return // Dead Gopher can't move 312 | } 313 | 314 | // Check if reached current target position: 315 | if int(Gopher.Pos.X) == Gopher.TargetPos.X && int(Gopher.Pos.Y) == Gopher.TargetPos.Y { 316 | // Check if we have more target positions in our path: 317 | if len(m.TargetPoss) > 0 { 318 | // Set the next target as the current 319 | Gopher.TargetPos = m.TargetPoss[0] 320 | // and remove it from the targets: 321 | m.TargetPoss = m.TargetPoss[:copy(m.TargetPoss, m.TargetPoss[1:])] 322 | } 323 | } 324 | 325 | // Step Gopher 326 | Gopher.step() 327 | 328 | // Check if Gopher reached the exit point 329 | if int(m.Gopher.Pos.X) == m.ExitPos.X && int(m.Gopher.Pos.Y) == m.ExitPos.Y { 330 | m.Won = true 331 | } 332 | } 333 | 334 | // stepBulldogs iterates over all Bulldogs, generates new random target if they reached their current, and steps them. 335 | func (e *Engine) stepBulldogs() { 336 | m := e.Model 337 | 338 | // Gopher's position: 339 | gpos := m.Gopher.Pos 340 | 341 | dirs := e.directions 342 | 343 | for _, bd := range m.Bulldogs { 344 | x, y := int(bd.Pos.X), int(bd.Pos.Y) 345 | 346 | if bd.TargetPos.X == x && bd.TargetPos.Y == y { 347 | row, col := y/BlockSize, x/BlockSize 348 | // Generate new, random target. 349 | // For this we shuffle all the directions, and check them sequentially. 350 | // Firts one in which direction there is a free path wins (such path surely exists). 351 | 352 | // Shuffle the directions slice: 353 | for i := len(dirs) - 1; i > 0; i-- { // last is already random, no use switching with itself 354 | r := rand.Intn(i + 1) 355 | dirs[i], dirs[r] = dirs[r], dirs[i] 356 | } 357 | 358 | var drow, dcol int 359 | for _, dir := range dirs { 360 | switch dir { 361 | case DirLeft: 362 | dcol = -1 363 | case DirRight: 364 | dcol = 1 365 | case DirUp: 366 | drow = -1 367 | case DirDown: 368 | drow = 1 369 | } 370 | if m.Lab[row+drow][col+dcol] == BlockEmpty { 371 | // Direction is good, check if we can even step 2 bocks in this way: 372 | if m.Lab[row+drow*2][col+dcol*2] == BlockEmpty { 373 | drow *= 2 374 | dcol *= 2 375 | } 376 | break 377 | } 378 | drow, dcol = 0, 0 379 | } 380 | 381 | bd.TargetPos.X += dcol * BlockSize 382 | bd.TargetPos.Y += drow * BlockSize 383 | } 384 | 385 | bd.step() 386 | 387 | if !m.Dead { 388 | // Check if this Bulldog reached Gopher (but only if not just won) 389 | if math.Abs(gpos.X-bd.Pos.X) < BlockSize*0.75 && math.Abs(gpos.Y-bd.Pos.Y) < BlockSize*0.75 && !m.Won { 390 | m.Dead = true // OK, we just died 391 | } 392 | } 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /engine/gen-lab.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "math/rand" 5 | ) 6 | 7 | // generateLab generates a new, random labyrinth. 8 | // lab must have odd number of rows and columns. 9 | func generateLab(lab [][]Block) { 10 | rows, cols := len(lab), len(lab[0]) 11 | 12 | // Create a "frame": 13 | for row := range lab { 14 | lab[row][0] = BlockWall 15 | lab[row][cols-1] = BlockWall 16 | } 17 | for col := range lab[0] { 18 | lab[0][col] = BlockWall 19 | lab[rows-1][col] = BlockWall 20 | } 21 | 22 | genLabArea(lab, 0, 0, rows-1, cols-1) 23 | } 24 | 25 | // genLabArea generates a random labyrinth inside the specified area, borders exclusive. 26 | // This is a recursive implementation, each iteration divides the area into 2 parts. 27 | func genLabArea(lab [][]Block, x1, y1, x2, y2 int) { 28 | dx, dy := x2-x1, y2-y1 29 | 30 | // Exit condition from the recursion: 31 | if dx <= 2 || dy <= 2 { 32 | return 33 | } 34 | 35 | // Decide if we do a vertical or horizontal split 36 | var vert bool 37 | if dy > dx { 38 | vert = false 39 | } else if dx > dy { 40 | vert = true 41 | } else if rand.Intn(2) == 0 { // Area is square, choose randomly 42 | vert = true 43 | } 44 | 45 | if vert { 46 | // Add vertical split 47 | var x int 48 | if dx > 6 { // To avoid long straight paths, only use random in smaller areas 49 | x = midWallPos(x1, x2) 50 | } else { 51 | x = rWallPos(x1, x2) 52 | } 53 | // A whole in it: 54 | y := rPassPos(y1, y2) 55 | for i := y1; i <= y2; i++ { 56 | if i != y { 57 | lab[i][x] = BlockWall 58 | } 59 | } 60 | 61 | genLabArea(lab, x1, y1, x, y2) 62 | genLabArea(lab, x, y1, x2, y2) 63 | } else { 64 | // Add horizontal split 65 | var y int 66 | if dy > 6 { // To avoid long straight paths, only use random in smaller areas 67 | y = midWallPos(y1, y2) 68 | } else { 69 | y = rWallPos(y1, y2) 70 | } 71 | // A whole in it: 72 | x := rPassPos(x1, x2) 73 | for i := x1; i <= x2; i++ { 74 | if i != x { 75 | lab[y][i] = BlockWall 76 | } 77 | } 78 | 79 | genLabArea(lab, x1, y1, x2, y) 80 | genLabArea(lab, x1, y, x2, y2) 81 | } 82 | } 83 | 84 | // rWallPos returns a random wall position which is an even number between the specified min and max. 85 | func rWallPos(min, max int) int { 86 | return min + (rand.Intn((max-min)/2-1)+1)*2 87 | } 88 | 89 | // midWallPos returns the wall position being at the middle of the specified min and max. 90 | func midWallPos(min, max int) int { 91 | n := (min + max) / 2 92 | // make sure it's even 93 | if n&0x01 == 1 { 94 | n-- 95 | } 96 | return n 97 | } 98 | 99 | // rPassPos returns a random passage position which is an odd number between the specified min and max. 100 | func rPassPos(min, max int) int { 101 | return rWallPos(min, max+2) - 1 102 | } 103 | -------------------------------------------------------------------------------- /engine/lab-size.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import "fmt" 4 | 5 | // LabSize difines choosable labyrinth sizes.. 6 | type LabSize struct { 7 | Name string 8 | rows int // Must be odd 9 | cols int // Must be odd 10 | Default bool 11 | } 12 | 13 | func (l *LabSize) String() string { 14 | return fmt.Sprintf("%s (%dx%d)", l.Name, l.rows, l.cols) 15 | } 16 | 17 | // LabSizes is a slice of all, ordered lab sizes. 18 | var LabSizes = []*LabSize{ 19 | &LabSize{Name: "XS", rows: 9, cols: 9}, 20 | &LabSize{Name: "S", rows: 15, cols: 15}, 21 | &LabSize{Name: "M", rows: 33, cols: 33, Default: true}, 22 | &LabSize{Name: "L", rows: 51, cols: 51}, 23 | &LabSize{Name: "XL", rows: 99, cols: 99}, 24 | } 25 | 26 | // LabSizeDefaultIdx is the index of the default lab size in LabSizes. 27 | var LabSizeDefaultIdx int 28 | 29 | func init() { 30 | for i, l := range LabSizes { 31 | if l.Default { 32 | LabSizeDefaultIdx = i 33 | break 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /engine/model.go: -------------------------------------------------------------------------------- 1 | // This file contains models of the game. 2 | 3 | package engine 4 | 5 | import ( 6 | "fmt" 7 | "image" 8 | "math" 9 | "sync" 10 | ) 11 | 12 | // Model is the model of the game. 13 | type Model struct { 14 | // Mutex to protect the model from concurrent access. 15 | sync.RWMutex 16 | 17 | // Game counter. Must be increased by one when a new game is initialized. 18 | // Can be used to invalidate caches when its value changes. 19 | Counter int 20 | 21 | // Size of the labyrinth in blocks. 22 | Rows, Cols int 23 | 24 | // Blocks of the lab. First indexed by row, then by column. 25 | Lab [][]Block 26 | 27 | // ExitPos: the position Gopher has to reach to win the game. 28 | ExitPos image.Point 29 | 30 | // Our well-beloved hero Gopher 31 | Gopher *MovingObj 32 | 33 | // The ancient enemies of Gopher: the bloodthirsty Bulldogs. 34 | Bulldogs []*MovingObj 35 | 36 | // Dead tells if Gopher is dead. 37 | Dead bool 38 | 39 | // Won tells if we won 40 | Won bool 41 | 42 | // For Gopher we maintain multiple target positions which specify a path on which Gopher will move along 43 | TargetPoss []image.Point 44 | } 45 | 46 | // Block is a square unit of the Labyrinth 47 | type Block int 48 | 49 | const ( 50 | // BlockEmpty is the empty, free-to-walk block 51 | BlockEmpty = iota 52 | // BlockWall designates an unpassable wall. 53 | BlockWall 54 | 55 | // BlockCount is not a valid block: just to tell how many blocks there are 56 | BlockCount 57 | ) 58 | 59 | // MovingObj describes moving objects in the labyrinth. 60 | type MovingObj struct { 61 | // The position in the labyrinth in pixel coordinates 62 | Pos struct { 63 | X, Y float64 64 | } 65 | 66 | // Direction this object is facing to 67 | Dir Dir 68 | 69 | // Target position this object is moving to 70 | TargetPos image.Point 71 | } 72 | 73 | // steps steps the MovingObj. 74 | func (m *MovingObj) step() { 75 | x, y := int(m.Pos.X), int(m.Pos.Y) 76 | 77 | // Only horizontal or vertical movement is allowed! 78 | if x != m.TargetPos.X { 79 | dx := math.Min(dt*v, math.Abs(float64(m.TargetPos.X)-m.Pos.X)) 80 | if x > m.TargetPos.X { 81 | dx = -dx 82 | m.Dir = DirLeft 83 | } else { 84 | m.Dir = DirRight 85 | } 86 | m.Pos.X += dx 87 | } else if y != m.TargetPos.Y { 88 | dy := math.Min(dt*v, math.Abs(float64(m.TargetPos.Y)-m.Pos.Y)) 89 | if y > m.TargetPos.Y { 90 | dy = -dy 91 | m.Dir = DirUp 92 | } else { 93 | m.Dir = DirDown 94 | } 95 | m.Pos.Y += dy 96 | } 97 | } 98 | 99 | // Dir represents directions 100 | type Dir int 101 | 102 | const ( 103 | // DirRight . 104 | DirRight = iota 105 | // DirLeft . 106 | DirLeft 107 | // DirUp . 108 | DirUp 109 | // DirDown . 110 | DirDown 111 | 112 | // DirCount is not a valid direction: just to tell how many directions there are 113 | DirCount 114 | ) 115 | 116 | func (d Dir) String() string { 117 | switch d { 118 | case DirRight: 119 | return "right" 120 | case DirLeft: 121 | return "left" 122 | case DirUp: 123 | return "up" 124 | case DirDown: 125 | return "down" 126 | } 127 | return fmt.Sprintf("Dir(%d)", d) 128 | } 129 | -------------------------------------------------------------------------------- /engine/speed.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // Speed of the game. 9 | type Speed struct { 10 | Name string 11 | 12 | loopDelay time.Duration 13 | 14 | Default bool 15 | } 16 | 17 | func (s *Speed) String() string { 18 | fps := (time.Second + s.loopDelay/2) / s.loopDelay // add half to make it round up from half 19 | return fmt.Sprintf("%s (%d FPS)", s.Name, fps) 20 | } 21 | 22 | // Speeds is a slice of all, ordered speeds. 23 | var Speeds = []*Speed{ 24 | &Speed{Name: "Slow", loopDelay: 67 * time.Millisecond}, // ~15 FPS 25 | &Speed{Name: "Normal", loopDelay: 50 * time.Millisecond, Default: true}, // ~20 FPS 26 | &Speed{Name: "Fast", loopDelay: 37 * time.Millisecond}, // ~27 FPS 27 | } 28 | 29 | // SpeedDefaultIdx is the index of the default speed in Speeds. 30 | var SpeedDefaultIdx int 31 | 32 | func init() { 33 | for i, s := range Speeds { 34 | if s.Default { 35 | SpeedDefaultIdx = i 36 | break 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/icza/golab 2 | 3 | go 1.13 4 | 5 | require ( 6 | gioui.org v0.0.0-20200213121532-69dfd2e3a554 7 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd // indirect 8 | golang.org/x/image v0.18.0 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 2 | gioui.org v0.0.0-20200213121532-69dfd2e3a554 h1:ukYMW1J9mfaImo98vr/+f1gk21GgKUevwnhWjgO2dxw= 3 | gioui.org v0.0.0-20200213121532-69dfd2e3a554/go.mod h1:AHI9rFr6AEEHCb8EPVtb/p5M+NMJRKH58IOp8O3Je04= 4 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 5 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 6 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 7 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 8 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 9 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 10 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 11 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 12 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 13 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 14 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 15 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 16 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 17 | golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= 18 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd h1:zkO/Lhoka23X63N9OSzpSeROEUQ5ODw47tM3YWjygbs= 19 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 20 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 21 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 22 | golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= 23 | golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= 24 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 25 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 26 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 27 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 28 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 29 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 30 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 31 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 32 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 33 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 34 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 35 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 36 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 37 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 38 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 39 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 40 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 41 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 42 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 43 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 44 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 45 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 46 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 47 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 48 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 49 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 50 | golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 51 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 52 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 53 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 54 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 55 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 56 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 57 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 58 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 59 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= 60 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 61 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 62 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 63 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 64 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 65 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 66 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 67 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 68 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 69 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 70 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 71 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 72 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 73 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 74 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 75 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 76 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 77 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 78 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 79 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 80 | golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 81 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 82 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 83 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 84 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 85 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 86 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 87 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 88 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 89 | -------------------------------------------------------------------------------- /screenshot-golab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icza/golab/59ee81c619e56dbe1302f3471d639ecde0e2c512/screenshot-golab.png -------------------------------------------------------------------------------- /view/_generate-embedded-imgs/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "fmt" 7 | "go/format" 8 | "io/ioutil" 9 | 10 | "github.com/icza/golab/engine" 11 | ) 12 | 13 | func main() { 14 | // Generate embedded-imgs.go source file, holding a map from file names to their base64 encoded contents. 15 | // Images are looked for in ../_images/. 16 | 17 | buf := &bytes.Buffer{} 18 | buf.WriteString(`// This file is generated by go generate. 19 | 20 | package view 21 | 22 | // Embedded images mapped from image (file) name to file content encoded in Base64 format. 23 | // Whether these are used depends on the useEmbeddedImages const in images.go. 24 | var base64Imgs = map[string]string{ 25 | `) 26 | 27 | var names []string 28 | for dir := engine.Dir(0); dir < engine.DirCount; dir++ { 29 | // Gopher images 30 | names = append(names, fmt.Sprintf("gopher-%s.png", dir)) 31 | // Bulldog images 32 | names = append(names, fmt.Sprintf("bulldog-%s.png", dir)) 33 | } 34 | 35 | names = append(names, "wall.png") 36 | names = append(names, "gopher-dead.png") 37 | names = append(names, "door.png") 38 | names = append(names, "marker.png") 39 | names = append(names, "won.png") 40 | 41 | // Generate map entries 42 | for _, name := range names { 43 | data, err := ioutil.ReadFile("../_images/" + name) 44 | if err != nil { 45 | panic(err) 46 | } 47 | 48 | fmt.Fprintf(buf, "\t\"%s\": \"%s\",\n", name, base64.StdEncoding.EncodeToString(data)) 49 | } 50 | 51 | buf.WriteString("}") 52 | 53 | formatted, err := format.Source(buf.Bytes()) 54 | if err != nil { 55 | panic(err) 56 | } 57 | 58 | if err := ioutil.WriteFile("embedded-imgs.go", formatted, 0664); err != nil { 59 | panic(err) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /view/embedded-imgs.go: -------------------------------------------------------------------------------- 1 | // This file is generated by go generate. 2 | 3 | package view 4 | 5 | // Embedded images mapped from image (file) name to file content encoded in Base64 format. 6 | // Whether these are used depends on the useEmbeddedImages const in images.go. 7 | var base64Imgs = map[string]string{ 8 | "gopher-right.png": "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", 9 | "bulldog-right.png": "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", 10 | "gopher-left.png": "iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAGw0lEQVRYw7WYa1RU1xXH//c5b4YBeQykCkLlIQhWKEWNWusjLtvaZNVUJBpM0yxr0pDSR1pj2q64TJemTTW1aboSiVlJmmqS1kdXsD5iBHRhfARjoyJBZhy5MjA4MzBz53nP6QdtG5YCM4N3fz3/s+9v9tl7nz2HQYLGieKUinUN0wW9rlqXMqFMNJryOVFjBQBKFL8SDvWGvJ7OoMfd5vy0vfnzpn2tAEi832Hi1WdXzSzP/cZ9h0tr16RwogiiKACld3RNyM01XquF7chBtGx6dqn3andT0QPfM1jyCvjzbzcGh6RrwfEATpv19K/fEU1JSe07Xt5QUrvmB1NXrJ4lXfgMesmOMFGgmfE1pGRYQb8AyWm1uLF3Nx4qL0ZuTg6OtLSixZIFS24+Wjdt6Pnqkz9PYwVBDAwMDL21qHoGgM64AQWtbl5NU/PRtKISREMh2I4eQnJxKea57Fg+swoGsxkAIDkceL7pCMjcxWAoBSUEsx0XsXLxwmH+JIcD9cc/gbVyJj59ewdYXsDUB1fh+pmT9P0V3zICkOM6yqV/fuPq2vM2yuv0tKRmNX2iy0Vf3NdER7JHDzTTxy/30tU7/0b9Ph/t6Oi4TbO3+Thdd7GH1tvd1JiZRZ9yDNLHO67T3IVLikcCYUcCNE/KYUkkAqoomPubzYgQgjnWVJjNZuTn59+2ITMwBPACpkVkFBYVoaCgAG1tbcM0RbmTAIZBRJax6A+vQAmHEA0E0H2oyRMvIPlw/Y9fEgwmLNzyEighoACuXL6MwcFB1NfXw+l0Dt+hKGAYBu5wCBqNBjk5ORBFcbjkC7WkTbZAOtWG/Y89dD8AaSRAfqSF3vazW/Y/WnOpZMWqvQAgMADNnoRdu3aBZVlkZGQM0w8kpUAj8HCWVqHj0iXY7HZMnjx5mGbbgcNg772Zm6kFxXh1et76oNezZ9R2Ntqi96otPG/jC/WcIAKE4JzkxKqKMsyqrv6fZtDjwU//0gi7HMae2u+g68i/0OryYMFXysFGIwgFAnB6B/Hsrr+DX7QMJBq5mUMsAzAQHcebXx+NgR9t0ZL35UJtsgVKOAwASJ5SiM3eMNy/+yOKTXpIcgD+0gpwhWU4WfcAhnr74JOugZ1Tja0yj+gNL6gShWg0AbMXgAT+X6hUUZA5vTLrViehCQGml5Zn3BZyQcSEby+H89bmJNmHXw4dxu/3boLT5YVRr0VWmhlvnngDp7/+GJhw6I6+KSFIumeiQWNKYkJDgzTeIgEAiEaT7s4lRMAQAhACMAxs0gBskhs9Xh6ukB52Rx8QCY8SF/z3mLXWiiom4SOmRBnz7mQ0OrxZuQ6/4n3gbwygvLwc393xDtLm14KNhMbaLRgzrUgYMOj1+GPp6oIShicaQN+t1qPheXDMmAEEAF4wGBIH9HR3uWO6dxQFfxqQEdBnYP9Hp2CuqAJVlJhuLE4QEwd0Xfh3f6wDD5uaDsOteSqOEYlhWG50v2M46At63VDNGDBgGWZcgD7pGtQ0hmHHFUFZ7u9TE49NzsmjiQJmPHlfSVXI71czgOxcqWVNdoalLC7A4vx7ZnQe2tq7bfuGtnBUUY2OgEFD3ZJXLx/c2r5kbnlDzIATUpLWppiNOH36IhhRoxogL4p47d2jEHgOwWDEGvM0Y+/pP2jQaewrf/Zy3fTauqeN1ix1MpBSbKypm9lp7939z6Nn3wIQjvdPU+rqDz92Ga3ZqgBSQvBaZeGDEdn/bqJVnGZSCQ4AWJ4Hy3Fp42kzqawoqtoHDemZpoQB06aWWaCyMTzHJgxoSE/Tqw0YdN+QEwbkeIFTE45Eowj7fFLCgEGvN6gmYDQQAK/VnksYsOfjE/0Mw6gHGAqShVu2d42niju8V23qHXEk4jq28Rk6HsD+aydP+FTpgYKAzz/Y0+m1XyHjAcTAxc+eUwPQ39eL41s2/nDMHzKW4NK+93b6rkvAXcxFlufhaD12DMD5u/LCaszMemTlgdYdvOYuTDYMA1fHBbJ72YIvjfZoFHMEAcDXKzW219eeDQVD42NjOVz/5AxyP9juBuCMZU8sjVj3/vaG9s1PLCvLtp3CmfNd4PNLwLAsKIntTZzTaOC1d0Paue1K4706yzfnV+jzJ2bO/8ehU6+POTOOJfjtT2oqeY6bqhAKkUblwL6dv2h8qqF73nMvPJIypXAGJ2om6lInQJtkBisIoJQgIssIDLgQ8fuCEVm+YPvo8LEzr2zbDMD5Xsb3z61YOmtav3tw9t3K6eStzzzs+euLP/ICSB9BkwdgMYDlAJYBqAKgHcnh+rX3t8ypLHo4lo//B55rzuqdnhrKAAAAAElFTkSuQmCC", 11 | "bulldog-left.png": "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", 12 | "gopher-up.png": "iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAEdklEQVRYw7XYfUxVZRwH8O95vRfuC3B5vYAEg4wQBQJDxQ1WyWTVWi0twDVgy96WW2zmRlSbzLasLS3nXKFzjVbD2sxaODReAs0UBSQhAkQQLpc34cLl3nNfzjn9of3nvdx7Ds/33/PcPZ8993l+5/ccCgrD8Pz6vLerc7jQkK0hpqgsXm9IY3iNGQBkSVwR3S6ry7Y4JCwuXJm+2fPHcNO5TgBSsPNQwY5PyN+WnfL0zosbyytNDMdDkkRAln3/gKZBMyxGfz+PjkMfPWsbH216/KVXdBGpj7F9350Sli0TghrgpoIDH3/PG4zGnpPHazPLK1/PeLmsgNFq/aIePhMF2etF5ycfTj657/1omuN45/z8ckPx1lwAQ0EDOW1IUWlTR6txXRJomsFYRwuSCgohejxQE4bjcauxARt274EkemHt7pJ/evV5PQDHw8bTvuDFX5z41pi4DpLbjd7TXyNxy3bVOAAQPW5k7CrDaGszZElCXE4elbKjJNnXeJ/AsEeSaVkUsWyZRGZ5BWRJwlpF8nrBh+rA8Bp4nU6MXmhaDBYotdS89yUoCrP9N9cU93+0pkhYrv2JX/bueRGAxeeW8PXAbp26NN17ozt2U3ZpeEramgN1sWb88FxRzcLtoXp/42h/D8c7WvsStmwHiciSiCfeeLdktXF+gRGpj6ZrwyPIAEURcTmb41crdX6BMRuzY0EosiTBmJik0xiMyoG83hACgqFoSmvOy1cOlCVRAtFQnD7OrHwPCrbFFbJAsJxOpxy4ODqyQBhIMRyvHDjX//ds8A1PkC0EzSgHApgRbAQXkQIFmqJUAe2WCRBeQlUr6HDMzhAtNOHJqbJSYOy+nZn5rhWiB5kutHRUJsRGZAUFzEhLzB26cMR69FjtFbdXJKaTQKG6ouSbf5uP9JQUZlcHDIwyGd80henR1TUAiteQK4I8j/ozreBYBoLgMQfcbo1NzjbrQjRjZfuPV+SUVxzQm+PJ7EBZRl1pxbahMWvjr603GgC4g700Rb7WcnVOb04g1jDUb07f7XGsnFF6iqMNhHAAQLMsaIaJVlNmImmeJ1oHdTFxBsXA6A1ZESAcimVoxUBdTHQoaaCwcM+hGMiwHEMSJ3m9cNvtFhX9oE0gCfQ6nWC12l7FwMmrl2cpily75XUJ0o7Dx0bUnOJB2/gdcn+xxzPXXveBrAY4O/HXZTuRGshxGP7t7JBt7LakBoj5gVsHSQBXZqy4dLjuLVUXdwD459yPp+1TFmAN9yLNsrjb2d4OoG/1pjuA6OPiq8rOd55kNWvQ2VAU5gb75cYXnkn099Eo4BW8/yHJcqrrndJrLpdbZf/MYKr7OoSvatoDwQUMBIC9xZlh++81wdt29v5LnmUDhjEaDZYmxjH4ee3oIcMA9Fru58DvVYEnvGrXU7XC0tLdxra+kaKDn1WZ1qfnMrwmKSQyClpjGGiOgyxL8DgccM7PwbNiFzwOR/+dtovt108c/RTANIA0AMMkgP6S+mBi44Om0wqgF4DqN9F/rNaaIny43Z0AAAAASUVORK5CYII=", 13 | "bulldog-up.png": "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", 14 | "gopher-down.png": "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", 15 | "bulldog-down.png": "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", 16 | "wall.png": "iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAIAAAADnC86AAABG0lEQVRYw+3Xva5EQBwF8EMYIgqtB+AFNqLUexS111OIRkJLKZFoJQoRJj62sDfXtZtbjdXMaRDJ/PKfjMQBbooAIAgC3/cNw5im6WpP1/UkScIwlAAYhmFZlm3b35xYBFBVVRRFcRwPw/AFsu97ABIARVHats2yDIDruoSQdV2Ze5Ik/XncL7Is932fpikA5vC6roSQx+NxtKXja0oppXS/Zwt/2IDf8y0I4k/YH6W3NcW7vmMOc5jDHOYwhznMYQ5zmMMMqtT7L/9FTeK07Bm+rjtRSlVV/QCLojjPc57nzMfd2yIAx3E0TftvYrbZtm1Zlr1/e56nKMoLJoQcN+GijONYlqVpml3XveC6ri+a9ZSiKJqm2eHb8gQldHKbOjeEqgAAAABJRU5ErkJggg==", 17 | "gopher-dead.png": "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", 18 | "door.png": "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", 19 | "marker.png": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACL0lEQVQ4y32ST0gVQRzHPzu7+54+s+wPmWnQRahOQV0KikKqQxHUpboU0SWyg0QEXoQiuhRG6EWwTkEQJEQkiRfBoKA6aUQGgYWapD3T3bc7M7szHXoPHq+1HwwMM7/Ph+/8ccio4dkzu7fW7+9W5vceg7F1TtP7b+Ho7ZPbXn6s7XVqF0ZnL3QL4d0JzA/yXhPGaOJkgUbRhrGm61jr4werCoamO446DiOOcKnPNdO0thWdBiwtzxDrBRzjonR68Fz7+HiFEdWCWC/fl2YJMEy+nafz8BBdR0aY+lAEDLEpotKgt5qpEQS7rEkJgxK9lz6xPJ8SLAj6r8yglCRNDZEO9lYzXmVy702LJ7XCdQVzn1fI5328XA7f9/FcwdxXxbo2jdaazATX980lSus4lpKWnZZCYz2+7+Pncqzf1MCG7QqpJFLrNDMBgJTqobV0Jibl6qNmJp5vwc+57DgxTRDGSKVRSj1d9RVujW1s8Fw38H0P1xUIYbBYTOKSpAlKJSSp3XyzY/Fn5iX2HFoMY6muhVFMqRQThppSqAmjiDCSxEp1VsOZHwngxnBhyhWi3XEcrLUYazDGvr57PDpQ2+tlCSIpTwMTFbvBMvnKnhUudSZFA6a8ZUWWoO9UOunAC5y/GeMlp39swCYmpQEoALlK+kwBgFxxLvueh+d6dvBi2gesKcP5am5VwcD5ZNYV7pPidzEI1AES+FUeUeUYHv+puCh63j2TBeBLGfqn/gA1mwntfiET+gAAAABJRU5ErkJggg==", 20 | "won.png": "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", 21 | } 22 | -------------------------------------------------------------------------------- /view/images.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "fmt" 7 | "image" 8 | "image/color" 9 | "image/draw" 10 | "image/png" 11 | "io/ioutil" 12 | 13 | "github.com/icza/golab/engine" 14 | ) 15 | 16 | //go:generate go run _generate-embedded-imgs/main.go 17 | 18 | // Tells if the embedded images are to be used. If false, images from files will be loaded. 19 | const useEmbeddedImages = true 20 | 21 | // imgGopher holds images of Gopher for each direction, each has zero Min point 22 | var imgGophers = make([]*image.RGBA, engine.DirCount) 23 | 24 | // imgDead is the Dead Gopher image. 25 | var imgDead *image.RGBA 26 | 27 | // imgBulldog holds images of a Bulldog for each direction, each has zero Min point 28 | var imgBulldogs = make([]*image.RGBA, engine.DirCount) 29 | 30 | // imgBlocks holds images of labyrinth blocks for each type, each has zero Min point 31 | var imgBlocks = make([]image.Image, engine.BlockCount) 32 | 33 | // imgMarker is the image of the path marker 34 | var imgMarker *image.RGBA 35 | 36 | // imgExit is the image of the exit sign 37 | var imgExit *image.RGBA 38 | 39 | // imgWon is the image of the winning sign 40 | var imgWon *image.RGBA 41 | 42 | func init() { 43 | for dir := engine.Dir(0); dir < engine.DirCount; dir++ { 44 | // Load Gopher images 45 | imgGophers[dir] = loadImg(fmt.Sprintf("gopher-%s.png", dir), true) 46 | // Load Bulldog images 47 | imgBulldogs[dir] = loadImg(fmt.Sprintf("bulldog-%s.png", dir), true) 48 | } 49 | 50 | imgBlocks[engine.BlockEmpty] = image.NewUniform(color.RGBA{A: 0xff}) 51 | imgBlocks[engine.BlockWall] = loadImg("wall.png", true) 52 | imgDead = loadImg("gopher-dead.png", true) 53 | imgExit = loadImg("door.png", true) 54 | 55 | imgMarker = loadImg("marker.png", false) 56 | imgWon = loadImg("won.png", false) 57 | } 58 | 59 | // loadImg loads a PNG image from the specified file, and converts it to image.RGBA and makes sure image has zero Min point. 60 | // This function only used during development as the result contains the images embedded. 61 | // blockSize tells if the image must be of the size of a block (else panics). 62 | func loadImg(name string, blockSize bool) *image.RGBA { 63 | var data []byte 64 | var err error 65 | 66 | if useEmbeddedImages { 67 | data, err = base64.StdEncoding.DecodeString(base64Imgs[name]) 68 | } else { 69 | data, err = ioutil.ReadFile(name) 70 | } 71 | if err != nil { 72 | panic(err) 73 | } 74 | return decodeImg(data, blockSize) 75 | } 76 | 77 | // decodeImg decodes an image from the specified data which must be of PNG format. 78 | // blockSize tells if the image must be of the size of a block (else panics). 79 | func decodeImg(data []byte, blockSize bool) *image.RGBA { 80 | src, err := png.Decode(bytes.NewBuffer(data)) 81 | if err != nil { 82 | panic(err) 83 | } 84 | 85 | // Convert to image.RGBA, also make sure result image has zero Min point 86 | b := src.Bounds() 87 | if blockSize && (b.Dx() != engine.BlockSize || b.Dy() != engine.BlockSize) { 88 | panic("Invalid image size!") 89 | } 90 | 91 | img := image.NewRGBA(image.Rect(0, 0, b.Dx(), b.Dy())) 92 | draw.Draw(img, src.Bounds(), src, b.Min, draw.Src) 93 | 94 | return img 95 | } 96 | -------------------------------------------------------------------------------- /view/options.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | "reflect" 7 | 8 | "gioui.org/layout" 9 | "gioui.org/unit" 10 | "gioui.org/widget" 11 | ) 12 | 13 | // options groups functionality to present a widget that can be used to select one option from many. 14 | // 15 | // This implementation uses a Button which loops through the possible values on clicks. 16 | type options struct { 17 | v *View 18 | title string 19 | values interface{} 20 | idx int // selected index 21 | 22 | btn *widget.Button 23 | } 24 | 25 | // newOptions creates a new options. 26 | // 27 | // values must be a slice of possible values. 28 | func newOptions(v *View, title string, values interface{}, defaultIdx int) *options { 29 | return &options{ 30 | v: v, 31 | title: title, 32 | values: values, 33 | idx: defaultIdx, 34 | btn: new(widget.Button), 35 | } 36 | } 37 | 38 | // handleInput handles user inputs that may change the selected option. 39 | func (o *options) handleInput() { 40 | for o.btn.Clicked(o.v.gtx) { 41 | o.onClick() 42 | } 43 | } 44 | 45 | // onClick does the job needed when the option is clicked: rotates the selected option. 46 | func (o *options) onClick() { 47 | o.idx = (o.idx + 1) % reflect.ValueOf(o.values).Len() 48 | } 49 | 50 | // selected returns the selected item. 51 | func (o *options) selected() interface{} { 52 | return reflect.ValueOf(o.values).Index(o.idx).Interface() 53 | } 54 | 55 | // layout lays out the UI widget 56 | func (o *options) layout() { 57 | layout.Inset{Left: unit.Px(5), Right: unit.Px(5)}.Layout(o.v.gtx, func() { 58 | b := o.v.th.Button( 59 | fmt.Sprintf("%s: %s", o.title, o.selected()), 60 | ) 61 | b.Background = color.RGBA{R: 100, G: 100, A: 255} 62 | b.Layout(o.v.gtx, o.btn) 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /view/view.go: -------------------------------------------------------------------------------- 1 | // Package view is the view of the game. It handles user input and presents the game to the user. 2 | package view 3 | 4 | import ( 5 | "fmt" 6 | "image" 7 | "image/color" 8 | "image/draw" 9 | "log" 10 | 11 | "gioui.org/app" 12 | "gioui.org/f32" 13 | "gioui.org/font" 14 | "gioui.org/font/opentype" 15 | "gioui.org/io/key" 16 | "gioui.org/io/pointer" 17 | "gioui.org/io/system" 18 | "gioui.org/layout" 19 | "gioui.org/op" 20 | "gioui.org/op/clip" 21 | "gioui.org/op/paint" 22 | "gioui.org/text" 23 | "gioui.org/unit" 24 | "gioui.org/widget" 25 | "gioui.org/widget/material" 26 | "github.com/icza/golab/engine" 27 | "golang.org/x/image/font/gofont/goregular" 28 | ) 29 | 30 | const ( 31 | controlsHeightPx = 70 32 | viewWidthPx = 700 33 | viewHeightPx = 700 34 | // WindowWidthPx is the suggested window width 35 | WindowWidthPx = viewWidthPx 36 | // WindowHeightPx is the suggested window height 37 | WindowHeightPx = controlsHeightPx + viewHeightPx 38 | ) 39 | 40 | func init() { 41 | // We only need font for buttons. gofont.Register() would do it, 42 | // but it registers all kinds of variants (like italic, mono, smallcaps etc.) 43 | // that we don't use, and they increase the output binary with almost 2MB. 44 | // So register only the the regular gofont: 45 | // gofont.Register() 46 | register := func(fnt text.Font, ttf []byte) { 47 | face, err := opentype.Parse(ttf) 48 | if err != nil { 49 | panic(fmt.Sprintf("failed to parse font: %v", err)) 50 | } 51 | fnt.Typeface = "Go" 52 | font.Register(fnt, face) 53 | } 54 | register(text.Font{}, goregular.TTF) 55 | } 56 | 57 | // imageOp wraps a paint.ImageOp and the source image. 58 | type imageOp struct { 59 | paint.ImageOp 60 | src image.Image 61 | } 62 | 63 | func newImageOp(src image.Image) imageOp { 64 | return imageOp{ 65 | ImageOp: paint.NewImageOp(src), 66 | src: src, 67 | } 68 | } 69 | 70 | // View displays the game window and handles user input. 71 | type View struct { 72 | engine *engine.Engine 73 | w *app.Window 74 | th *material.Theme 75 | gtx *layout.Context 76 | 77 | // New Game button model 78 | newGameBtn *widget.Button 79 | 80 | // Difficulty options 81 | diffOpt *options 82 | // Lab size options 83 | labSizeOpt *options 84 | // Speed options 85 | speedOpt *options 86 | 87 | // Height of controls in pixels 88 | controlsHeightPx int 89 | 90 | // "static" imageOps 91 | imgOpGophers []imageOp 92 | imgOpDead imageOp 93 | imgOpBulldogs []imageOp 94 | imgOpMarker imageOp 95 | imgOpExit imageOp 96 | imgOpWon imageOp 97 | 98 | // gameCounter for the cached data 99 | gameCounter int 100 | // cached ImageOp of the whole labyrinth (only the blocks) 101 | labImgOp imageOp 102 | 103 | // Tells what offset was last applied to draw the lab view. 104 | // Used when calculating click position in the lab. 105 | labViewOffset f32.Point 106 | // labViewClip tells what clip rectangle was applied to draw the lab view. 107 | // Used to tell if a click is accepted in the lab. 108 | labViewClip f32.Rectangle 109 | } 110 | 111 | // New returns a new View. 112 | func New(eng *engine.Engine, w *app.Window) *View { 113 | v := &View{ 114 | engine: eng, 115 | w: w, 116 | th: material.NewTheme(), 117 | gtx: layout.NewContext((w.Queue())), 118 | newGameBtn: new(widget.Button), 119 | imgOpDead: newImageOp(imgDead), 120 | imgOpMarker: newImageOp(imgMarker), 121 | imgOpExit: newImageOp(imgExit), 122 | imgOpWon: newImageOp(imgWon), 123 | } 124 | 125 | for _, img := range imgGophers { 126 | v.imgOpGophers = append(v.imgOpGophers, newImageOp(img)) 127 | } 128 | for _, img := range imgBulldogs { 129 | v.imgOpBulldogs = append(v.imgOpBulldogs, newImageOp(img)) 130 | } 131 | 132 | v.diffOpt = newOptions(v, "[D]ifficulty", engine.Difficulties, engine.DifficultyDefaultIdx) 133 | v.labSizeOpt = newOptions(v, "[L]ab size", engine.LabSizes, engine.LabSizeDefaultIdx) 134 | v.speedOpt = newOptions(v, "[S]peed", engine.Speeds, engine.SpeedDefaultIdx) 135 | 136 | return v 137 | } 138 | 139 | // Loop starts handing user input and frame redraws. 140 | // This function returns only if the user closes the app. 141 | func (v *View) Loop() { 142 | for e := range v.w.Events() { 143 | switch e := e.(type) { 144 | case system.FrameEvent: 145 | v.drawFrame(e) 146 | case pointer.Event: 147 | // TODO maybe send click on Release? 148 | if e.Type == pointer.Press { 149 | pos := e.Position.Sub(v.labViewOffset) 150 | // apply clip 151 | r := v.labViewClip 152 | if pos.X >= r.Min.X && pos.X < r.Max.X && 153 | pos.Y >= r.Min.Y && pos.Y < r.Max.Y { 154 | // TODO if e.Source == pointer.Touch, set left button? 155 | v.engine.SendClick(engine.Click{ 156 | X: int(pos.X), 157 | Y: int(pos.Y), 158 | Left: e.Buttons&pointer.ButtonLeft != 0, 159 | Right: e.Buttons&pointer.ButtonRight != 0, 160 | }) 161 | } 162 | } 163 | case key.Event: 164 | sendKey := func(dir engine.Dir) { 165 | v.engine.SendKey(engine.Key{DirKeys: map[engine.Dir]bool{dir: true}}) 166 | } 167 | switch e.Name { 168 | case key.NameLeftArrow: 169 | sendKey(engine.DirLeft) 170 | case key.NameRightArrow: 171 | sendKey(engine.DirRight) 172 | case key.NameUpArrow: 173 | sendKey(engine.DirUp) 174 | case key.NameDownArrow: 175 | sendKey(engine.DirDown) 176 | } 177 | if e.Modifiers&key.ModAlt != 0 { 178 | switch e.Name { 179 | case "N": 180 | v.sendNewGame() 181 | case "D": 182 | v.diffOpt.onClick() 183 | case "L": 184 | v.labSizeOpt.onClick() 185 | case "S": 186 | v.speedOpt.onClick() 187 | } 188 | } 189 | case system.DestroyEvent: 190 | log.Println("Goodbye!") 191 | } 192 | } 193 | } 194 | 195 | // drawFrame draws a frame of the window. 196 | func (v *View) drawFrame(e system.FrameEvent) { 197 | gtx := v.gtx 198 | 199 | gtx.Reset(e.Config, e.Size) 200 | 201 | // Handle button clicks 202 | for v.newGameBtn.Clicked(v.gtx) { 203 | v.sendNewGame() 204 | } 205 | v.diffOpt.handleInput() 206 | v.labSizeOpt.handleInput() 207 | v.speedOpt.handleInput() 208 | 209 | v.drawControls() 210 | v.drawLab() 211 | 212 | e.Frame(gtx.Ops) 213 | } 214 | 215 | // sendNewGame sends a new game command to the engine. 216 | func (v *View) sendNewGame() { 217 | v.engine.NewGame(engine.GameConfig{ 218 | Difficulty: v.diffOpt.selected().(*engine.Difficulty), 219 | LabSize: v.labSizeOpt.selected().(*engine.LabSize), 220 | Speed: v.speedOpt.selected().(*engine.Speed), 221 | }) 222 | } 223 | 224 | // drawControls draws the control and setup widgets. 225 | func (v *View) drawControls() { 226 | th, gtx := v.th, v.gtx 227 | 228 | layout.N.Layout(gtx, func() { 229 | layout.UniformInset(unit.Px(5)).Layout(gtx, func() { 230 | layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, 231 | layout.Rigid(func() { 232 | layout.Inset{Left: unit.Px(10), Right: unit.Px(10)}.Layout(gtx, func() { 233 | b := th.Button("[N]ew Game") 234 | b.Background = color.RGBA{R: 20, G: 130, B: 20, A: 255} 235 | b.Layout(gtx, v.newGameBtn) 236 | }) 237 | }), 238 | layout.Rigid(v.diffOpt.layout), 239 | layout.Rigid(v.labSizeOpt.layout), 240 | layout.Rigid(v.speedOpt.layout), 241 | ) 242 | }) 243 | v.controlsHeightPx = gtx.Dimensions.Size.Y 244 | }) 245 | } 246 | 247 | // drawLab draws the labyrinth. 248 | func (v *View) drawLab() { 249 | m := v.engine.Model 250 | m.RLock() 251 | defer m.RUnlock() 252 | 253 | gtx := v.gtx 254 | 255 | // Victory image must be drawn while locking but 256 | // after transformations undone. 257 | defer func() { 258 | if m.Won { 259 | v.drawImg(v.imgOpWon, 260 | v.labViewOffset.X+v.labViewClip.Min.X+(v.labViewClip.Dx()-float32(v.imgOpWon.src.Bounds().Dx()))/2, 261 | v.labViewOffset.Y+v.labViewClip.Min.Y+(v.labViewClip.Dy()-float32(v.imgOpWon.src.Bounds().Dy()))/2, 262 | ) 263 | } 264 | }() 265 | 266 | var stack op.StackOp 267 | stack.Push(gtx.Ops) 268 | defer stack.Pop() 269 | 270 | // Center lab view in window: 271 | displayWidth, displayHeight := float32(viewWidthPx), float32(viewHeightPx) 272 | labWidth := float32(m.Cols * engine.BlockSize) 273 | labHeight := float32(m.Rows * engine.BlockSize) 274 | if labWidth < displayWidth { 275 | displayWidth = labWidth 276 | } 277 | if labHeight < displayHeight { 278 | displayHeight = labHeight 279 | } 280 | 281 | // Calculate the visible window of the lab image. 282 | // Try to center Gopher in view: 283 | rect := f32.Rectangle{} 284 | rect.Max = f32.Point{X: displayWidth, Y: displayHeight} 285 | rect = rect.Add(f32.Point{ 286 | X: float32(m.Gopher.Pos.X) - displayWidth/2, 287 | Y: float32(m.Gopher.Pos.Y) - displayHeight/2, 288 | }) 289 | // But needs correction at the edges of the view (it can't be centered) 290 | corr := f32.Point{} 291 | if rect.Min.X < 0 { 292 | corr.X = -rect.Min.X 293 | } 294 | if rect.Min.Y < 0 { 295 | corr.Y = -rect.Min.Y 296 | } 297 | if rect.Max.X > labWidth { 298 | corr.X = labWidth - rect.Max.X 299 | } 300 | if rect.Max.Y > labHeight { 301 | corr.Y = labHeight - rect.Max.Y 302 | } 303 | rect = rect.Add(corr) 304 | 305 | v.labViewOffset = f32.Point{ 306 | X: (float32(gtx.Constraints.Width.Max) - displayWidth) / 2, 307 | Y: float32(v.controlsHeightPx), 308 | }.Sub(rect.Min) 309 | op.TransformOp{}.Offset(v.labViewOffset).Add(gtx.Ops) 310 | v.labViewClip = rect 311 | clip.Rect{Rect: v.labViewClip}.Op(gtx.Ops).Add(gtx.Ops) 312 | 313 | // First the blocks: 314 | v.ensureLabImgOp() 315 | v.drawImg(v.labImgOp, 0, 0) 316 | 317 | // Now objects in the lab: 318 | // TODO do not draw images outside of the view 319 | 320 | // Draw target position markers: 321 | mbounds := imgMarker.Bounds() 322 | tp := m.Gopher.TargetPos 323 | v.drawImg(v.imgOpMarker, float32(tp.X-mbounds.Dx()/2), float32(tp.Y-mbounds.Dy()/2)) 324 | for _, tp := range m.TargetPoss { 325 | v.drawImg(v.imgOpMarker, float32(tp.X-mbounds.Dx()/2), float32(tp.Y-mbounds.Dy()/2)) 326 | } 327 | // Gopher: 328 | if m.Dead { 329 | v.drawObj(v.imgOpDead, m.Gopher) 330 | } else { 331 | v.drawObj(v.imgOpGophers[m.Gopher.Dir], m.Gopher) 332 | } 333 | // Bulldogs: 334 | for _, bd := range m.Bulldogs { 335 | v.drawObj(v.imgOpBulldogs[bd.Dir], bd) 336 | } 337 | } 338 | 339 | // drawObj draws the given image of the given moving obj. 340 | func (v *View) drawObj(iop imageOp, obj *engine.MovingObj) { 341 | v.drawImg(iop, float32(obj.Pos.X-engine.BlockSize/2), float32(obj.Pos.Y-engine.BlockSize/2)) 342 | } 343 | 344 | // drawImg draws the given image to the given position. 345 | func (v *View) drawImg(iop imageOp, x, y float32) { 346 | var stack op.StackOp 347 | stack.Push(v.gtx.Ops) 348 | 349 | op.TransformOp{}.Offset(f32.Point{X: x, Y: y}).Add(v.gtx.Ops) 350 | 351 | iop.Add(v.gtx.Ops) 352 | imgBounds := iop.src.Bounds() 353 | paint.PaintOp{Rect: f32.Rectangle{ 354 | Max: f32.Point{X: float32(imgBounds.Max.X), Y: float32(imgBounds.Max.Y)}, 355 | }}.Add(v.gtx.Ops) 356 | 357 | stack.Pop() 358 | } 359 | 360 | // ensureLabImgOp ensures labImgOp is up-to-date 361 | func (v *View) ensureLabImgOp() { 362 | m := v.engine.Model 363 | if v.gameCounter == m.Counter { 364 | // We have the lab image of the current game 365 | return 366 | } 367 | 368 | labImg := image.NewRGBA(image.Rect(0, 0, m.Cols*engine.BlockSize, m.Rows*engine.BlockSize)) 369 | var r image.Rectangle 370 | for row := range m.Lab { 371 | r.Min.Y = row * engine.BlockSize 372 | r.Max.Y = r.Min.Y + engine.BlockSize 373 | for col, block := range m.Lab[row] { 374 | r.Min.X = col * engine.BlockSize 375 | r.Max.X = r.Min.X + engine.BlockSize 376 | src := imgBlocks[block] 377 | draw.Draw(labImg, r, src, image.Point{}, draw.Over) 378 | } 379 | } 380 | 381 | // Exit sign: 382 | r.Min = m.ExitPos 383 | r.Min = r.Min.Add(image.Point{-engine.BlockSize / 2, -engine.BlockSize / 2}) 384 | r.Max = r.Min.Add(image.Point{engine.BlockSize, engine.BlockSize}) 385 | draw.Draw(labImg, r, imgExit, image.Point{}, draw.Over) 386 | 387 | v.labImgOp = newImageOp(labImg) 388 | 389 | v.gameCounter = m.Counter 390 | } 391 | --------------------------------------------------------------------------------