diff --git a/.github/workflows/pages.yaml b/.github/workflows/pages.yaml new file mode 100644 index 0000000..87eceae --- /dev/null +++ b/.github/workflows/pages.yaml @@ -0,0 +1,57 @@ +name: CI + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Install rust toolchain + uses: dtolnay/rust-toolchain@beta + with: + targets: wasm32-unknown-unknown + + - name: Install trunk + uses: taiki-e/install-action@v2 + with: + tool: trunk + + - name: Checkout + uses: actions/checkout@v4 + + - name: Build website + run: trunk build + + - name: Upload artifact + uses: actions/upload-pages-artifact@v2 + with: + path: dist + + deploy: + needs: build + runs-on: ubuntu-latest + + if: github.event_name != 'pull_request' + + permissions: + pages: write + id-token: write + + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v3 + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..897a8f7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +/dist +.DS_Store diff --git a/README.md b/README.md index b7aa15d..8603df3 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,51 @@ # Mindsweeper — a principled take on minesweeper -To play, visit https://alexbuz.github.io/mindsweeper/. Once the page loads, no further internet connection is required. +To play, visit https://alexbuz.github.io/mindsweeper/. Once the page loads, no further internet +connection is required. ## Background -Traditional minesweeper is a game of logical deduction—until it's not. Sometimes, you end up in a situation where there is not enough information to find a tile that is definitely safe to reveal. In such cases, guessing is required to proceed, and that often leads to the loss of an otherwise smooth-sailing game. However, there's no reason it has to be that way. It's merely a consequence of the random manner in which mines are typically arranged at the start of the game. Some mine arrangements happen to necessitate guessing, while others do not. This is a matter of luck, and it's not a particularly fun aspect of a game that is otherwise about logic. +Traditional minesweeper is a game of logical deduction—until it's not. Sometimes, you end up in a +situation where there is not enough information to find a tile that is definitely safe to reveal. In +such cases, guessing is required to proceed, and that often leads to the loss of an otherwise +smooth-sailing game. However, there's no reason it has to be that way. It's merely a consequence of +the random manner in which mines are typically arranged at the start of the game. Some mine +arrangements happen to necessitate guessing, while others do not. This is a matter of luck, and it's +not a particularly fun aspect of a game that is otherwise about logic. -Eliminating the need for guesswork, then, is a matter of modifying the mine arrangement algorithm. Rather than simply placing each mine under a random tile, it should instead consider each mine arrangement as a whole, choosing a random mine arrangement from the set of mine arrangements that would allow a perfect logician to win without guessing. Ideally, it should sample uniformly from that set of mine arrangements, ensuring that every such arrangement is equally likely to be chosen. That is precisely what mindsweeper is designed to do, and it accomplishes this within a matter of milliseconds after you make your first click. +Eliminating the need for guesswork, then, is a matter of modifying the mine arrangement algorithm. +Rather than simply placing each mine under a random tile, it should instead consider each mine +arrangement as a whole, choosing a random mine arrangement from the set of mine arrangements that +would allow a perfect logician to win without guessing. Ideally, it should sample uniformly from +that set of mine arrangements, ensuring that every such arrangement is equally likely to be chosen. +That is precisely what mindsweeper is designed to do, and it accomplishes this within a matter of +milliseconds after you make your first click. ## Features 1. Guessing is *never* necessary - There's no need to toggle a setting. Mindsweeper is a game of pure skill, always. 2. Guess punishment - - Since guessing is already unnecessary, it's only natural to take the idea of "no guessing" a step further and forbid guessing entirely. This feature, enabled by default, effectively rids the game of all remaining luck aspects. If you click on a tile that *can* be a mine, then it *will* be a mine, guaranteed. + - Since guessing is already unnecessary, it's only natural to take the idea of "no guessing" a + step further and forbid guessing entirely. This feature, enabled by default, effectively rids + the game of all remaining luck aspects. If you click on a tile that *can* be a mine, then it + *will* be a mine, guaranteed. 3. Unrestricted first click - - Mindsweeper does not obligate you to click a particular tile to start the game. The mine arrangement algorithm works on demand, and is fast enough to avoid introducing any delay. + - Mindsweeper does not obligate you to click a particular tile to start the game. The mine + arrangement algorithm works on demand, and is fast enough to avoid introducing any delay. 4. Uniform sampling - - All mine arrangements are viable, except those that necessitate guessing. If a particular mine arrangement is solvable by a perfect logician without guessing, then it's just as likely to be picked by the algorithm as every other viable arrangement. + - All mine arrangements are viable, except those that necessitate guessing. If a particular mine + arrangement is solvable by a perfect logician without guessing, then it's just as likely to be + picked by the algorithm as every other viable arrangement. 5. Post-mortem analysis - - If you reveal a mine and lose the game, you'll get feedback that helps you improve. You get to see which flags you misplaced (if any), as well as which tiles you could (and could not) have safely revealed. Tiles are color-coded to show this information at a glance, and you can also hover over any tile to see this explained in words. + - If you reveal a mine and lose the game, you'll get feedback that helps you improve. You get to + see which flags you misplaced (if any), as well as which tiles you could (and could not) have + safely revealed. Tiles are color-coded to show this information at a glance, and you can also + hover over any tile to see this explained in words. 6. High performance - - Mindsweeper is written in Rust and compiles to WASM. When you make your first click, the mine arrangement algorithm generally finishes running before you even release the mouse button, so there is no first-click delay. + - Mindsweeper is written in Rust and compiles to WASM. When you make your first click, the mine + arrangement algorithm generally finishes running before you even release the mouse button, so + there is no first-click delay. 7. Completely offline - Mindsweeper does not depend on a server. All of the code runs locally in your browser. @@ -35,6 +59,8 @@ cd mindsweeper trunk build ``` -The built files will be placed in the `dist` directory, the contents of which must be placed in `docs` to be served by GitHub Pages. +The built files will be placed in the `dist` directory, the contents of which must be served to the +user. -For development, instead of `trunk build`, you can run `trunk serve --public-url=/ --open` to start a local server that automatically rebuilds the project when you make changes. \ No newline at end of file +For development, instead of `trunk build`, you can run `trunk serve --public-url=/ --open` to start +a local server that automatically rebuilds the project when you make changes. diff --git a/benches/solvable_bench.rs b/benches/solvable_bench.rs index 09dd500..71418a2 100644 --- a/benches/solvable_bench.rs +++ b/benches/solvable_bench.rs @@ -6,7 +6,7 @@ fn criterion_benchmark(c: &mut Criterion) { let game_config = GameConfig { grid_config: GridConfig::expert(), mode: GameMode::Normal, - punish_guessing: true, + ..Default::default() }; b.iter(|| LocalGame::new(game_config, game_config.grid_config.random_tile_id())) }); @@ -14,7 +14,7 @@ fn criterion_benchmark(c: &mut Criterion) { let game_config = GameConfig { grid_config: GridConfig::expert(), mode: GameMode::Mindless, - punish_guessing: true, + ..Default::default() }; b.iter(|| LocalGame::new(game_config, game_config.grid_config.random_tile_id())) }); diff --git a/docs/favicon-243584e0eb10e903.svg b/docs/favicon-243584e0eb10e903.svg deleted file mode 100644 index 9c3b84b..0000000 --- a/docs/favicon-243584e0eb10e903.svg +++ /dev/null @@ -1,4 +0,0 @@ - - 🧠 - 🧹 - \ No newline at end of file diff --git a/docs/favicon-46b4b2a00dd13992.png b/docs/favicon-46b4b2a00dd13992.png deleted file mode 100644 index e6d5e02..0000000 Binary files a/docs/favicon-46b4b2a00dd13992.png and /dev/null differ diff --git a/docs/index.html b/docs/index.html deleted file mode 100644 index 7cc0156..0000000 --- a/docs/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - Mindsweeper - - - - - - - \ No newline at end of file diff --git a/docs/main-610f006d6bf581a5.css b/docs/main-610f006d6bf581a5.css deleted file mode 100644 index afe1796..0000000 --- a/docs/main-610f006d6bf581a5.css +++ /dev/null @@ -1,168 +0,0 @@ -body { - font-family: Arial, Helvetica, sans-serif; -} - -h2 { - margin-bottom: 0; - padding-right: 30px; -} - -dialog :not(h2) { - line-height: 1.25; -} - -li { - margin-top: 5px; -} - -li li { - font-size: 85%; -} - -dialog { - border: none; - box-shadow: 0 0 16px gray; - border-radius: 20px; - max-width: calc(min(80ch, 90vw)); - max-height: 90vh; - padding: 0; -} - -dialog>div { - padding: 0 20px 5px 20px; -} - -dialog::backdrop { - background: rgba(0, 0, 0, 0.2); -} - -label { - -webkit-user-select: none; - user-select: none; -} - -button#close-dialog { - position: absolute; - width: 28px; - height: 28px; - top: 15px; - right: 15px; -} - -table { - margin: auto; - border-collapse: collapse; - -webkit-user-select: none; - user-select: none; - cursor: default; -} - -thead td>div { - display: flex; - justify-content: space-evenly; -} - -thead td { - height: 30px; - font-size: 16px; -} - -.timer { - padding: 1px 3px; - border-radius: 3px; -} - -tbody:not(.punish-guessing) { - --shadow-red: 128; -} - -tbody.autopilot { - --shadow-green: 128; -} - -tbody.mindless { - --shadow-blue: 128; -} - -tbody { - box-shadow: 8px 8px 8px rgba(var(--shadow-red, 0), var(--shadow-green, 0), var(--shadow-blue, 0), 0.2); -} - -.tile { - height: 36px; - width: 36px; - text-align: center; - border: 1px solid black; - font-size: 24px; - font-weight: bold; - font-family: 'Courier New', Courier, monospace; - background-color: #eee; -} - -.hidden { - display: none; -} - -.bg-red { - background-color: pink; -} - -.bg-green { - background-color: lightgreen; -} - -.bg-blue { - background-color: lightblue; -} - -.bg-orange { - background-color: #ffcc99; -} - -.bg-yellow { - background-color: #ffff99; -} - -.text-faded { - color: rgba(0, 0, 0, 0.5); -} - -.text-red { - color: red; -} - -.revealed { - background-color: #ccc; -} - -.number-1 { - color: blue; -} - -.number-2 { - color: green; -} - -.number-3 { - color: red; -} - -.number-4 { - color: purple; -} - -.number-5 { - color: maroon; -} - -.number-6 { - color: teal; -} - -.number-7 { - color: black; -} - -.number-8 { - color: gray; -} \ No newline at end of file diff --git a/docs/mindsweeper-c3849c404b610118.js b/docs/mindsweeper-c3849c404b610118.js deleted file mode 100644 index 21200c2..0000000 --- a/docs/mindsweeper-c3849c404b610118.js +++ /dev/null @@ -1,838 +0,0 @@ -let wasm; - -const heap = new Array(128).fill(undefined); - -heap.push(undefined, null, true, false); - -function getObject(idx) { return heap[idx]; } - -let heap_next = heap.length; - -function dropObject(idx) { - if (idx < 132) return; - heap[idx] = heap_next; - heap_next = idx; -} - -function takeObject(idx) { - const ret = getObject(idx); - dropObject(idx); - return ret; -} - -function addHeapObject(obj) { - if (heap_next === heap.length) heap.push(heap.length + 1); - const idx = heap_next; - heap_next = heap[idx]; - - heap[idx] = obj; - return idx; -} - -let WASM_VECTOR_LEN = 0; - -let cachedUint8Memory0 = null; - -function getUint8Memory0() { - if (cachedUint8Memory0 === null || cachedUint8Memory0.byteLength === 0) { - cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer); - } - return cachedUint8Memory0; -} - -const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } ); - -const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' - ? function (arg, view) { - return cachedTextEncoder.encodeInto(arg, view); -} - : function (arg, view) { - const buf = cachedTextEncoder.encode(arg); - view.set(buf); - return { - read: arg.length, - written: buf.length - }; -}); - -function passStringToWasm0(arg, malloc, realloc) { - - if (realloc === undefined) { - const buf = cachedTextEncoder.encode(arg); - const ptr = malloc(buf.length, 1) >>> 0; - getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf); - WASM_VECTOR_LEN = buf.length; - return ptr; - } - - let len = arg.length; - let ptr = malloc(len, 1) >>> 0; - - const mem = getUint8Memory0(); - - let offset = 0; - - for (; offset < len; offset++) { - const code = arg.charCodeAt(offset); - if (code > 0x7F) break; - mem[ptr + offset] = code; - } - - if (offset !== len) { - if (offset !== 0) { - arg = arg.slice(offset); - } - ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; - const view = getUint8Memory0().subarray(ptr + offset, ptr + len); - const ret = encodeString(arg, view); - - offset += ret.written; - } - - WASM_VECTOR_LEN = offset; - return ptr; -} - -function isLikeNone(x) { - return x === undefined || x === null; -} - -let cachedInt32Memory0 = null; - -function getInt32Memory0() { - if (cachedInt32Memory0 === null || cachedInt32Memory0.byteLength === 0) { - cachedInt32Memory0 = new Int32Array(wasm.memory.buffer); - } - return cachedInt32Memory0; -} - -const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } ); - -if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); }; - -function getStringFromWasm0(ptr, len) { - ptr = ptr >>> 0; - return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); -} - -function debugString(val) { - // primitive types - const type = typeof val; - if (type == 'number' || type == 'boolean' || val == null) { - return `${val}`; - } - if (type == 'string') { - return `"${val}"`; - } - if (type == 'symbol') { - const description = val.description; - if (description == null) { - return 'Symbol'; - } else { - return `Symbol(${description})`; - } - } - if (type == 'function') { - const name = val.name; - if (typeof name == 'string' && name.length > 0) { - return `Function(${name})`; - } else { - return 'Function'; - } - } - // objects - if (Array.isArray(val)) { - const length = val.length; - let debug = '['; - if (length > 0) { - debug += debugString(val[0]); - } - for(let i = 1; i < length; i++) { - debug += ', ' + debugString(val[i]); - } - debug += ']'; - return debug; - } - // Test for built-in - const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); - let className; - if (builtInMatches.length > 1) { - className = builtInMatches[1]; - } else { - // Failed to match the standard '[object ClassName]' - return toString.call(val); - } - if (className == 'Object') { - // we're a user defined class or Object - // JSON.stringify avoids problems with cycles, and is generally much - // easier than looping through ownProperties of `val`. - try { - return 'Object(' + JSON.stringify(val) + ')'; - } catch (_) { - return 'Object'; - } - } - // errors - if (val instanceof Error) { - return `${val.name}: ${val.message}\n${val.stack}`; - } - // TODO we could test for more things here, like `Set`s and `Map`s. - return className; -} - -function makeMutClosure(arg0, arg1, dtor, f) { - const state = { a: arg0, b: arg1, cnt: 1, dtor }; - const real = (...args) => { - // First up with a closure we increment the internal reference - // count. This ensures that the Rust closure environment won't - // be deallocated while we're invoking it. - state.cnt++; - const a = state.a; - state.a = 0; - try { - return f(a, state.b, ...args); - } finally { - if (--state.cnt === 0) { - wasm.__wbindgen_export_2.get(state.dtor)(a, state.b); - - } else { - state.a = a; - } - } - }; - real.original = state; - - return real; -} -function __wbg_adapter_26(arg0, arg1) { - wasm.__wbindgen_export_3(arg0, arg1); -} - -function __wbg_adapter_29(arg0, arg1, arg2) { - wasm.__wbindgen_export_4(arg0, arg1, addHeapObject(arg2)); -} - -function makeClosure(arg0, arg1, dtor, f) { - const state = { a: arg0, b: arg1, cnt: 1, dtor }; - const real = (...args) => { - // First up with a closure we increment the internal reference - // count. This ensures that the Rust closure environment won't - // be deallocated while we're invoking it. - state.cnt++; - try { - return f(state.a, state.b, ...args); - } finally { - if (--state.cnt === 0) { - wasm.__wbindgen_export_2.get(state.dtor)(state.a, state.b); - state.a = 0; - - } - } - }; - real.original = state; - - return real; -} - -let stack_pointer = 128; - -function addBorrowedObject(obj) { - if (stack_pointer == 1) throw new Error('out of js stack'); - heap[--stack_pointer] = obj; - return stack_pointer; -} -function __wbg_adapter_32(arg0, arg1, arg2) { - try { - wasm.__wbindgen_export_5(arg0, arg1, addBorrowedObject(arg2)); - } finally { - heap[stack_pointer++] = undefined; - } -} - -function handleError(f, args) { - try { - return f.apply(this, args); - } catch (e) { - wasm.__wbindgen_export_7(addHeapObject(e)); - } -} - -let cachedUint32Memory0 = null; - -function getUint32Memory0() { - if (cachedUint32Memory0 === null || cachedUint32Memory0.byteLength === 0) { - cachedUint32Memory0 = new Uint32Array(wasm.memory.buffer); - } - return cachedUint32Memory0; -} - -function getArrayJsValueFromWasm0(ptr, len) { - ptr = ptr >>> 0; - const mem = getUint32Memory0(); - const slice = mem.subarray(ptr / 4, ptr / 4 + len); - const result = []; - for (let i = 0; i < slice.length; i++) { - result.push(takeObject(slice[i])); - } - return result; -} - -async function __wbg_load(module, imports) { - if (typeof Response === 'function' && module instanceof Response) { - if (typeof WebAssembly.instantiateStreaming === 'function') { - try { - return await WebAssembly.instantiateStreaming(module, imports); - - } catch (e) { - if (module.headers.get('Content-Type') != 'application/wasm') { - console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); - - } else { - throw e; - } - } - } - - const bytes = await module.arrayBuffer(); - return await WebAssembly.instantiate(bytes, imports); - - } else { - const instance = await WebAssembly.instantiate(module, imports); - - if (instance instanceof WebAssembly.Instance) { - return { instance, module }; - - } else { - return instance; - } - } -} - -function __wbg_get_imports() { - const imports = {}; - imports.wbg = {}; - imports.wbg.__wbg_getItem_5395a7e200c31e89 = function() { return handleError(function (arg0, arg1, arg2, arg3) { - const ret = getObject(arg1).getItem(getStringFromWasm0(arg2, arg3)); - var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export_0, wasm.__wbindgen_export_1); - var len1 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len1; - getInt32Memory0()[arg0 / 4 + 0] = ptr1; - }, arguments) }; - imports.wbg.__wbindgen_object_drop_ref = function(arg0) { - takeObject(arg0); - }; - imports.wbg.__wbindgen_object_clone_ref = function(arg0) { - const ret = getObject(arg0); - return addHeapObject(ret); - }; - imports.wbg.__wbg_clearInterval_7f51e4380e64c6c5 = function(arg0) { - const ret = clearInterval(takeObject(arg0)); - return addHeapObject(ret); - }; - imports.wbg.__wbindgen_cb_drop = function(arg0) { - const obj = takeObject(arg0).original; - if (obj.cnt-- == 1) { - obj.a = 0; - return true; - } - const ret = false; - return ret; - }; - imports.wbg.__wbg_new0_622c21a64f3d83ea = function() { - const ret = new Date(); - return addHeapObject(ret); - }; - imports.wbg.__wbg_setInterval_e227d4d8a9d44d66 = function() { return handleError(function (arg0, arg1) { - const ret = setInterval(getObject(arg0), arg1); - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_getTime_9272be78826033e1 = function(arg0) { - const ret = getObject(arg0).getTime(); - return ret; - }; - imports.wbg.__wbg_setItem_3786c4c8dd0c9bd0 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { - getObject(arg0).setItem(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); - }, arguments) }; - imports.wbg.__wbg_showModal_d83825761768ee45 = function() { return handleError(function (arg0) { - getObject(arg0).showModal(); - }, arguments) }; - imports.wbg.__wbg_close_41f50a570edca7fb = function(arg0) { - getObject(arg0).close(); - }; - imports.wbg.__wbg_target_52ddf6955f636bf5 = function(arg0) { - const ret = getObject(arg0).target; - return isLikeNone(ret) ? 0 : addHeapObject(ret); - }; - imports.wbg.__wbg_checked_f46acdc11342a4bd = function(arg0) { - const ret = getObject(arg0).checked; - return ret; - }; - imports.wbg.__wbg_value_30ed7fed7e3a14ba = function(arg0, arg1) { - const ret = getObject(arg1).value; - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export_0, wasm.__wbindgen_export_1); - const len1 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len1; - getInt32Memory0()[arg0 / 4 + 0] = ptr1; - }; - imports.wbg.__wbg_button_cd87b6dabbde9631 = function(arg0) { - const ret = getObject(arg0).button; - return ret; - }; - imports.wbg.__wbg_buttons_45faa2de9fb9d23b = function(arg0) { - const ret = getObject(arg0).buttons; - return ret; - }; - imports.wbg.__wbg_stopPropagation_b7a931152e09c2ab = function(arg0) { - getObject(arg0).stopPropagation(); - }; - imports.wbg.__wbg_body_64abc9aba1891e91 = function(arg0) { - const ret = getObject(arg0).body; - return isLikeNone(ret) ? 0 : addHeapObject(ret); - }; - imports.wbg.__wbg_lastChild_a62e3fbaab87f734 = function(arg0) { - const ret = getObject(arg0).lastChild; - return isLikeNone(ret) ? 0 : addHeapObject(ret); - }; - imports.wbg.__wbg_removeChild_942eb9c02243d84d = function() { return handleError(function (arg0, arg1) { - const ret = getObject(arg0).removeChild(getObject(arg1)); - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_new_abda76e883ba8a5f = function() { - const ret = new Error(); - return addHeapObject(ret); - }; - imports.wbg.__wbg_stack_658279fe44541cf6 = function(arg0, arg1) { - const ret = getObject(arg1).stack; - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export_0, wasm.__wbindgen_export_1); - const len1 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len1; - getInt32Memory0()[arg0 / 4 + 0] = ptr1; - }; - imports.wbg.__wbg_error_f851667af71bcfc6 = function(arg0, arg1) { - let deferred0_0; - let deferred0_1; - try { - deferred0_0 = arg0; - deferred0_1 = arg1; - console.error(getStringFromWasm0(arg0, arg1)); - } finally { - wasm.__wbindgen_export_6(deferred0_0, deferred0_1, 1); - } - }; - imports.wbg.__wbg_instanceof_Error_31ca8d97f188bfbc = function(arg0) { - let result; - try { - result = getObject(arg0) instanceof Error; - } catch (_) { - result = false; - } - const ret = result; - return ret; - }; - imports.wbg.__wbg_name_e5eede664187fed6 = function(arg0) { - const ret = getObject(arg0).name; - return addHeapObject(ret); - }; - imports.wbg.__wbindgen_string_get = function(arg0, arg1) { - const obj = getObject(arg1); - const ret = typeof(obj) === 'string' ? obj : undefined; - var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export_0, wasm.__wbindgen_export_1); - var len1 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len1; - getInt32Memory0()[arg0 / 4 + 0] = ptr1; - }; - imports.wbg.__wbg_message_55b9ea8030688597 = function(arg0) { - const ret = getObject(arg0).message; - return addHeapObject(ret); - }; - imports.wbg.__wbg_toString_a44236e90224e279 = function(arg0) { - const ret = getObject(arg0).toString(); - return addHeapObject(ret); - }; - imports.wbg.__wbg_instanceof_Window_3e5cd1f48c152d01 = function(arg0) { - let result; - try { - result = getObject(arg0) instanceof Window; - } catch (_) { - result = false; - } - const ret = result; - return ret; - }; - imports.wbg.__wbg_localStorage_8c507fd281456944 = function() { return handleError(function (arg0) { - const ret = getObject(arg0).localStorage; - return isLikeNone(ret) ? 0 : addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_document_d609202d16c38224 = function(arg0) { - const ret = getObject(arg0).document; - return isLikeNone(ret) ? 0 : addHeapObject(ret); - }; - imports.wbg.__wbg_self_f0e34d89f33b99fd = function() { return handleError(function () { - const ret = self.self; - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_window_d3b084224f4774d7 = function() { return handleError(function () { - const ret = window.window; - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_globalThis_9caa27ff917c6860 = function() { return handleError(function () { - const ret = globalThis.globalThis; - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_global_35dfdd59a4da3e74 = function() { return handleError(function () { - const ret = global.global; - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbindgen_is_undefined = function(arg0) { - const ret = getObject(arg0) === undefined; - return ret; - }; - imports.wbg.__wbg_newnoargs_c62ea9419c21fbac = function(arg0, arg1) { - const ret = new Function(getStringFromWasm0(arg0, arg1)); - return addHeapObject(ret); - }; - imports.wbg.__wbg_call_90c26b09837aba1c = function() { return handleError(function (arg0, arg1) { - const ret = getObject(arg0).call(getObject(arg1)); - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_call_5da1969d7cd31ccd = function() { return handleError(function (arg0, arg1, arg2) { - const ret = getObject(arg0).call(getObject(arg1), getObject(arg2)); - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_crypto_58f13aa23ffcb166 = function(arg0) { - const ret = getObject(arg0).crypto; - return addHeapObject(ret); - }; - imports.wbg.__wbindgen_is_object = function(arg0) { - const val = getObject(arg0); - const ret = typeof(val) === 'object' && val !== null; - return ret; - }; - imports.wbg.__wbg_process_5b786e71d465a513 = function(arg0) { - const ret = getObject(arg0).process; - return addHeapObject(ret); - }; - imports.wbg.__wbg_versions_c2ab80650590b6a2 = function(arg0) { - const ret = getObject(arg0).versions; - return addHeapObject(ret); - }; - imports.wbg.__wbg_node_523d7bd03ef69fba = function(arg0) { - const ret = getObject(arg0).node; - return addHeapObject(ret); - }; - imports.wbg.__wbindgen_is_string = function(arg0) { - const ret = typeof(getObject(arg0)) === 'string'; - return ret; - }; - imports.wbg.__wbg_msCrypto_abcb1295e768d1f2 = function(arg0) { - const ret = getObject(arg0).msCrypto; - return addHeapObject(ret); - }; - imports.wbg.__wbg_newwithlength_6c2df9e2f3028c43 = function(arg0) { - const ret = new Uint8Array(arg0 >>> 0); - return addHeapObject(ret); - }; - imports.wbg.__wbg_require_2784e593a4674877 = function() { return handleError(function () { - const ret = module.require; - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbindgen_is_function = function(arg0) { - const ret = typeof(getObject(arg0)) === 'function'; - return ret; - }; - imports.wbg.__wbindgen_string_new = function(arg0, arg1) { - const ret = getStringFromWasm0(arg0, arg1); - return addHeapObject(ret); - }; - imports.wbg.__wbg_subarray_2e940e41c0f5a1d9 = function(arg0, arg1, arg2) { - const ret = getObject(arg0).subarray(arg1 >>> 0, arg2 >>> 0); - return addHeapObject(ret); - }; - imports.wbg.__wbg_getRandomValues_504510b5564925af = function() { return handleError(function (arg0, arg1) { - getObject(arg0).getRandomValues(getObject(arg1)); - }, arguments) }; - imports.wbg.__wbindgen_memory = function() { - const ret = wasm.memory; - return addHeapObject(ret); - }; - imports.wbg.__wbg_buffer_a448f833075b71ba = function(arg0) { - const ret = getObject(arg0).buffer; - return addHeapObject(ret); - }; - imports.wbg.__wbg_newwithbyteoffsetandlength_d0482f893617af71 = function(arg0, arg1, arg2) { - const ret = new Uint8Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0); - return addHeapObject(ret); - }; - imports.wbg.__wbg_randomFillSync_a0d98aa11c81fe89 = function() { return handleError(function (arg0, arg1) { - getObject(arg0).randomFillSync(takeObject(arg1)); - }, arguments) }; - imports.wbg.__wbg_new_8f67e318f15d7254 = function(arg0) { - const ret = new Uint8Array(getObject(arg0)); - return addHeapObject(ret); - }; - imports.wbg.__wbg_set_2357bf09366ee480 = function(arg0, arg1, arg2) { - getObject(arg0).set(getObject(arg1), arg2 >>> 0); - }; - imports.wbg.__wbindgen_debug_string = function(arg0, arg1) { - const ret = debugString(getObject(arg1)); - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export_0, wasm.__wbindgen_export_1); - const len1 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len1; - getInt32Memory0()[arg0 / 4 + 0] = ptr1; - }; - imports.wbg.__wbindgen_throw = function(arg0, arg1) { - throw new Error(getStringFromWasm0(arg0, arg1)); - }; - imports.wbg.__wbg_queueMicrotask_adae4bc085237231 = function(arg0) { - const ret = getObject(arg0).queueMicrotask; - return addHeapObject(ret); - }; - imports.wbg.__wbg_resolve_6e1c6553a82f85b7 = function(arg0) { - const ret = Promise.resolve(getObject(arg0)); - return addHeapObject(ret); - }; - imports.wbg.__wbg_then_3ab08cd4fbb91ae9 = function(arg0, arg1) { - const ret = getObject(arg0).then(getObject(arg1)); - return addHeapObject(ret); - }; - imports.wbg.__wbg_queueMicrotask_4d890031a6a5a50c = function(arg0) { - queueMicrotask(getObject(arg0)); - }; - imports.wbg.__wbg_is_ff7acd231c75c0e4 = function(arg0, arg1) { - const ret = Object.is(getObject(arg0), getObject(arg1)); - return ret; - }; - imports.wbg.__wbg_nextSibling_bafccd3347d24543 = function(arg0) { - const ret = getObject(arg0).nextSibling; - return isLikeNone(ret) ? 0 : addHeapObject(ret); - }; - imports.wbg.__wbg_insertBefore_726c1640c419e940 = function() { return handleError(function (arg0, arg1, arg2) { - const ret = getObject(arg0).insertBefore(getObject(arg1), getObject(arg2)); - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_error_a526fb08a0205972 = function(arg0, arg1) { - var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice(); - wasm.__wbindgen_export_6(arg0, arg1 * 4, 4); - console.error(...v0); - }; - imports.wbg.__wbg_setnodeValue_630c6470d05b600e = function(arg0, arg1, arg2) { - getObject(arg0).nodeValue = arg1 === 0 ? undefined : getStringFromWasm0(arg1, arg2); - }; - imports.wbg.__wbg_namespaceURI_7cc7ef157e398356 = function(arg0, arg1) { - const ret = getObject(arg1).namespaceURI; - var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export_0, wasm.__wbindgen_export_1); - var len1 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len1; - getInt32Memory0()[arg0 / 4 + 0] = ptr1; - }; - imports.wbg.__wbg_createElementNS_524b05a6070757b6 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { - const ret = getObject(arg0).createElementNS(arg1 === 0 ? undefined : getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_cloneNode_405d5ea3f7e0098a = function() { return handleError(function (arg0) { - const ret = getObject(arg0).cloneNode(); - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_createElement_fdd5c113cb84539e = function() { return handleError(function (arg0, arg1, arg2) { - const ret = getObject(arg0).createElement(getStringFromWasm0(arg1, arg2)); - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_setchecked_c1d5c3726082e274 = function(arg0, arg1) { - getObject(arg0).checked = arg1 !== 0; - }; - imports.wbg.__wbg_setvalue_5b3442ff620b4a5d = function(arg0, arg1, arg2) { - getObject(arg0).value = getStringFromWasm0(arg1, arg2); - }; - imports.wbg.__wbg_setvalue_a11f3069fd7a1805 = function(arg0, arg1, arg2) { - getObject(arg0).value = getStringFromWasm0(arg1, arg2); - }; - imports.wbg.__wbg_createTextNode_7ff0c034b2855f66 = function(arg0, arg1, arg2) { - const ret = getObject(arg0).createTextNode(getStringFromWasm0(arg1, arg2)); - return addHeapObject(ret); - }; - imports.wbg.__wbg_setinnerHTML_ce0d6527ce4086f2 = function(arg0, arg1, arg2) { - getObject(arg0).innerHTML = getStringFromWasm0(arg1, arg2); - }; - imports.wbg.__wbg_childNodes_a5762b4b3e073cf6 = function(arg0) { - const ret = getObject(arg0).childNodes; - return addHeapObject(ret); - }; - imports.wbg.__wbg_from_71add2e723d1f1b2 = function(arg0) { - const ret = Array.from(getObject(arg0)); - return addHeapObject(ret); - }; - imports.wbg.__wbg_length_1009b1af0c481d7b = function(arg0) { - const ret = getObject(arg0).length; - return ret; - }; - imports.wbg.__wbg_get_f01601b5a68d10e3 = function(arg0, arg1) { - const ret = getObject(arg0)[arg1 >>> 0]; - return addHeapObject(ret); - }; - imports.wbg.__wbg_setsubtreeid_e1fab6b578c800cf = function(arg0, arg1) { - getObject(arg0).__yew_subtree_id = arg1 >>> 0; - }; - imports.wbg.__wbg_new_9fb8d994e1c0aaac = function() { - const ret = new Object(); - return addHeapObject(ret); - }; - imports.wbg.__wbg_set_759f75cd92b612d2 = function() { return handleError(function (arg0, arg1, arg2) { - const ret = Reflect.set(getObject(arg0), getObject(arg1), getObject(arg2)); - return ret; - }, arguments) }; - imports.wbg.__wbg_addEventListener_374cbfd2bbc19ccf = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { - getObject(arg0).addEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3), getObject(arg4)); - }, arguments) }; - imports.wbg.__wbg_composedPath_12a068e57a98cf90 = function(arg0) { - const ret = getObject(arg0).composedPath(); - return addHeapObject(ret); - }; - imports.wbg.__wbg_cachekey_b81c1aacc6a0645c = function(arg0, arg1) { - const ret = getObject(arg1).__yew_subtree_cache_key; - getInt32Memory0()[arg0 / 4 + 1] = isLikeNone(ret) ? 0 : ret; - getInt32Memory0()[arg0 / 4 + 0] = !isLikeNone(ret); - }; - imports.wbg.__wbg_subtreeid_e80a1798fee782f9 = function(arg0, arg1) { - const ret = getObject(arg1).__yew_subtree_id; - getInt32Memory0()[arg0 / 4 + 1] = isLikeNone(ret) ? 0 : ret; - getInt32Memory0()[arg0 / 4 + 0] = !isLikeNone(ret); - }; - imports.wbg.__wbg_instanceof_Element_3f326a19cc457941 = function(arg0) { - let result; - try { - result = getObject(arg0) instanceof Element; - } catch (_) { - result = false; - } - const ret = result; - return ret; - }; - imports.wbg.__wbg_bubbles_f1cdd0584446cad0 = function(arg0) { - const ret = getObject(arg0).bubbles; - return ret; - }; - imports.wbg.__wbg_parentElement_72e144c2e8d9e0b5 = function(arg0) { - const ret = getObject(arg0).parentElement; - return isLikeNone(ret) ? 0 : addHeapObject(ret); - }; - imports.wbg.__wbg_parentNode_92a7017b3a4fad43 = function(arg0) { - const ret = getObject(arg0).parentNode; - return isLikeNone(ret) ? 0 : addHeapObject(ret); - }; - imports.wbg.__wbg_instanceof_ShadowRoot_0bd39e89ab117f86 = function(arg0) { - let result; - try { - result = getObject(arg0) instanceof ShadowRoot; - } catch (_) { - result = false; - } - const ret = result; - return ret; - }; - imports.wbg.__wbg_host_09eee5e3d9cf59a1 = function(arg0) { - const ret = getObject(arg0).host; - return addHeapObject(ret); - }; - imports.wbg.__wbg_setcachekey_75bcd45312087529 = function(arg0, arg1) { - getObject(arg0).__yew_subtree_cache_key = arg1 >>> 0; - }; - imports.wbg.__wbg_cancelBubble_976cfdf7ac449a6c = function(arg0) { - const ret = getObject(arg0).cancelBubble; - return ret; - }; - imports.wbg.__wbg_listenerid_6dcf1c62b7b7de58 = function(arg0, arg1) { - const ret = getObject(arg1).__yew_listener_id; - getInt32Memory0()[arg0 / 4 + 1] = isLikeNone(ret) ? 0 : ret; - getInt32Memory0()[arg0 / 4 + 0] = !isLikeNone(ret); - }; - imports.wbg.__wbg_setlistenerid_f2e783343fa0cec1 = function(arg0, arg1) { - getObject(arg0).__yew_listener_id = arg1 >>> 0; - }; - imports.wbg.__wbg_setAttribute_e7b72a5e7cfcb5a3 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { - getObject(arg0).setAttribute(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); - }, arguments) }; - imports.wbg.__wbg_value_e024243a9dae20bc = function(arg0, arg1) { - const ret = getObject(arg1).value; - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export_0, wasm.__wbindgen_export_1); - const len1 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len1; - getInt32Memory0()[arg0 / 4 + 0] = ptr1; - }; - imports.wbg.__wbg_value_57e57170f6952449 = function(arg0, arg1) { - const ret = getObject(arg1).value; - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export_0, wasm.__wbindgen_export_1); - const len1 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len1; - getInt32Memory0()[arg0 / 4 + 0] = ptr1; - }; - imports.wbg.__wbg_removeAttribute_2e200daefb9f3ed4 = function() { return handleError(function (arg0, arg1, arg2) { - getObject(arg0).removeAttribute(getStringFromWasm0(arg1, arg2)); - }, arguments) }; - imports.wbg.__wbindgen_closure_wrapper189 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 11, __wbg_adapter_26); - return addHeapObject(ret); - }; - imports.wbg.__wbindgen_closure_wrapper1070 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 11, __wbg_adapter_29); - return addHeapObject(ret); - }; - imports.wbg.__wbindgen_closure_wrapper1842 = function(arg0, arg1, arg2) { - const ret = makeClosure(arg0, arg1, 11, __wbg_adapter_32); - return addHeapObject(ret); - }; - - return imports; -} - -function __wbg_init_memory(imports, maybe_memory) { - -} - -function __wbg_finalize_init(instance, module) { - wasm = instance.exports; - __wbg_init.__wbindgen_wasm_module = module; - cachedInt32Memory0 = null; - cachedUint32Memory0 = null; - cachedUint8Memory0 = null; - - wasm.__wbindgen_start(); - return wasm; -} - -function initSync(module) { - if (wasm !== undefined) return wasm; - - const imports = __wbg_get_imports(); - - __wbg_init_memory(imports); - - if (!(module instanceof WebAssembly.Module)) { - module = new WebAssembly.Module(module); - } - - const instance = new WebAssembly.Instance(module, imports); - - return __wbg_finalize_init(instance, module); -} - -async function __wbg_init(input) { - if (wasm !== undefined) return wasm; - - if (typeof input === 'undefined') { - input = new URL('mindsweeper-c3849c404b610118_bg.wasm', import.meta.url); - } - const imports = __wbg_get_imports(); - - if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) { - input = fetch(input); - } - - __wbg_init_memory(imports); - - const { instance, module } = await __wbg_load(await input, imports); - - return __wbg_finalize_init(instance, module); -} - -export { initSync } -export default __wbg_init; diff --git a/docs/mindsweeper-c3849c404b610118_bg.wasm b/docs/mindsweeper-c3849c404b610118_bg.wasm deleted file mode 100644 index 67529f9..0000000 Binary files a/docs/mindsweeper-c3849c404b610118_bg.wasm and /dev/null differ diff --git a/index.html b/index.html index a7fe64e..d371bb5 100644 --- a/index.html +++ b/index.html @@ -1,10 +1,12 @@ - - - Mindsweeper - - - - + + + + Mindsweeper + + + + + \ No newline at end of file diff --git a/main.css b/main.css index afe1796..d639e07 100644 --- a/main.css +++ b/main.css @@ -70,6 +70,11 @@ thead td { .timer { padding: 1px 3px; border-radius: 3px; + font-family: 'Menlo', 'Consolas', monospace; +} + +.timer.faded { + color: gray; } tbody:not(.punish-guessing) { diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 5d56faf..d0cc4ff 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,3 @@ [toolchain] -channel = "nightly" +channel = "beta" # For rust 1.75 - Move to stable when it is released +targets = ["wasm32-unknown-unknown"] diff --git a/src/analyzer.rs b/src/analyzer.rs index 3674d19..61c4377 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -4,7 +4,7 @@ use crate::{ utils::*, }; use itertools::{izip, Itertools}; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, BTreeSet}; use tinyvec::array_vec; diff --git a/src/client/flag.rs b/src/client/flag.rs index 65acbc7..8d600e3 100644 --- a/src/client/flag.rs +++ b/src/client/flag.rs @@ -1,5 +1,6 @@ use std::collections::BTreeMap; +#[derive(Debug)] pub enum Flag { Tentative, Permanent, diff --git a/src/client/mod.rs b/src/client/mod.rs index 57bb868..2f9fa07 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -25,10 +25,11 @@ pub enum Msg { ShowDialog, CloseDialog, NewGame, - SetNumbersStyle(NumbersStyle), SetGridConfig(GridConfig), SetGameMode(GameMode), SetPunishGuessing(bool), + SetNumbersStyle(NumbersStyle), + SetSubtractFlags(bool), } #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default, EnumIter, Display)] @@ -47,22 +48,13 @@ impl NumbersStyle { } } -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize)] +#[serde(default)] struct Theme { - #[serde(default)] numbers_style: NumbersStyle, + subtract_flags: bool, } -static DEFAULT_GAME_CONFIG: GameConfig = GameConfig { - grid_config: GridConfig::beginner(), - mode: GameMode::Normal, - punish_guessing: true, -}; - -static DEFAULT_THEME: Theme = Theme { - numbers_style: NumbersStyle::Digits, -}; - pub struct Client { dialog_ref: NodeRef, should_show_dialog: bool, @@ -225,9 +217,28 @@ impl Client { if let Some(game) = self.game.as_ref() { if let Some(adjacent_mine_count) = game.adjacent_mine_count(tile_id) { tile_classes.push("revealed"); + if adjacent_mine_count > 0 { - tile_classes.push(format!("number-{adjacent_mine_count}")); - tile_contents.push(self.theme.numbers_style.render(adjacent_mine_count)); + let display_count = if self.theme.subtract_flags { + let adjacent_flags: u8 = self + .game_config + .grid_config + .iter_adjacent(tile_id) + .map(|tile_id| u8::from(self.flags.get(tile_id).is_some())) + .sum(); + + adjacent_mine_count.checked_sub(adjacent_flags) + } else { + Some(adjacent_mine_count) + }; + + if let Some(count) = display_count { + tile_classes.push(format!("number-{count}")); + tile_contents.push(self.theme.numbers_style.render(count)); + } else { + tile_classes.push("text-red"); + tile_contents.push('?') + } } } else if game.status().is_won() { tile_contents.push(FLAG_SYMBOL); @@ -243,7 +254,8 @@ impl Client { tile_classes.push("text-faded"); } if analyzer_tile.is_known_mine() { - tooltip = Some("This was definitely a mine, so you were correct to flag it."); + tooltip = + Some("This was definitely a mine, so you were correct to flag it."); tile_classes.push("bg-green"); } else if analyzer_tile.is_known_safe() { tooltip = Some("This was definitely safe, so you were wrong to flag it."); @@ -263,19 +275,24 @@ impl Client { tooltip = Some("This may or may not have been a mine, so you were wrong to reveal it. In this case, it was in fact a mine, so you lost."); tile_classes.push("bg-orange"); } else { - tooltip = Some("This may or may not have been a mine, and in this case it was."); + tooltip = Some( + "This may or may not have been a mine, and in this case it was.", + ); } } else if self.last_revealed.contains(&tile_id) { - tooltip = Some("This was definitely a mine, and you revealed it, so you lost."); + tooltip = + Some("This was definitely a mine, and you revealed it, so you lost."); tile_classes.push("bg-red"); } else { - tooltip = Some("This was definitely a mine, so you could've safely flagged it."); + tooltip = + Some("This was definitely a mine, so you could've safely flagged it."); } } else if analyzer_tile.is_known_safe() { tooltip = Some("This was definitely safe, so you could've safely revealed it."); tile_classes.push("bg-blue"); } else { - tooltip = Some("This may or may not have been a mine, and in this case it was not."); + tooltip = + Some("This may or may not have been a mine, and in this case it was not."); } } else if let Some(flag) = self.flags.get(tile_id) { tile_contents.push(FLAG_SYMBOL); @@ -287,10 +304,15 @@ impl Client { html! { + onmousedown={scope.callback(move |e: MouseEvent| + Msg::TileMouseEvent { tile_id, button: e.button(), buttons: e.buttons() } + )} + onmouseup={scope.callback(move |e: MouseEvent| + Msg::TileMouseEvent { tile_id, button: e.button(), buttons: e.buttons() } + )}>
{ tile_contents }
@@ -316,8 +338,8 @@ impl Component for Client { dialog_ref: NodeRef::default(), should_show_dialog: stored_game_config.is_err() || !LocalStorage::get::(storage_keys::CLOSED_DIALOG).unwrap_or_default(), - game_config: stored_game_config.unwrap_or(DEFAULT_GAME_CONFIG), - theme: LocalStorage::get(storage_keys::THEME).unwrap_or(DEFAULT_THEME), + game_config: stored_game_config.unwrap_or_default(), + theme: LocalStorage::get(storage_keys::THEME).unwrap_or_default(), prepared_game: None, game: None, flags: FlagStore::new(), @@ -363,10 +385,6 @@ impl Component for Client { Msg::ShowDialog => self.show_dialog(), Msg::CloseDialog => self.close_dialog(), Msg::NewGame => self.new_game(), - Msg::SetNumbersStyle(style) => { - self.theme.numbers_style = style; - self.save_theme(); - } Msg::SetGridConfig(config) => { self.game_config.grid_config = config; self.save_game_config(); @@ -382,6 +400,14 @@ impl Component for Client { self.save_game_config(); self.new_game(); } + Msg::SetNumbersStyle(style) => { + self.theme.numbers_style = style; + self.save_theme(); + } + Msg::SetSubtractFlags(value) => { + self.theme.subtract_flags = value; + self.save_theme(); + } } true } @@ -445,8 +471,8 @@ impl Component for Client { .map(|config| (FloatOrd(config.mine_density()), config)) .chain([ ( - FloatOrd(DEFAULT_GAME_CONFIG.grid_config.mine_density()), - DEFAULT_GAME_CONFIG.grid_config, + FloatOrd(GridConfig::default().mine_density()), + GridConfig::default(), ), ( FloatOrd(self.game_config.grid_config.mine_density()), @@ -544,6 +570,25 @@ impl Component for Client { } +
  • + +
      +
    • + { "This subtracts the number of adjacent flags from the number displayed on each revealed tile, so that you can see at a glance how many flags you have left to place." } +
    • +
    +
  • diff --git a/src/client/timer.rs b/src/client/timer.rs index 473b17b..e550ce9 100644 --- a/src/client/timer.rs +++ b/src/client/timer.rs @@ -6,7 +6,7 @@ use gloo::{ use itertools::Itertools; use js_sys::Date; use mindsweeper::server::GameConfig; -use std::collections::BTreeMap; +use std::{collections::BTreeMap, fmt}; use yew::prelude::*; #[derive(Debug, PartialEq)] @@ -44,6 +44,25 @@ impl Timer { } } +struct TimerElapsed(f64); + +impl fmt::Display for TimerElapsed { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let cs = (self.0 % 1.0 * 100.0) as u64; + let s = (self.0 % 60.0) as u64; + let mut m = (self.0 / 60.0) as u64; + + let h = m / 60; + m %= 60; + + if h > 0 { + write!(f, "{h:02}:")?; + } + + write!(f, "{m:02}:{s:02}.{cs:02}") + } +} + impl Component for Timer { type Message = TimerMsg; type Properties = TimerProps; @@ -106,11 +125,9 @@ impl Component for Timer { let props = ctx.props(); let best = self.best_times.get(&props.game_config).copied(); let mut timer_classes = classes!("timer"); - let content = if let TimerMode::Reset = props.timer_mode { - match best { - Some(best) => format!("Best: {best:.02}"), - None => String::from("Best: N/A"), - } + let time = if let TimerMode::Reset = props.timer_mode { + timer_classes.push("faded"); + best } else { let time = self.elapsed_secs(); if let TimerMode::Stopped { won_game: true } = props.timer_mode { @@ -118,11 +135,15 @@ impl Component for Timer { timer_classes.push("bg-green"); } } - format!("Time: {time:.02}") + Some(time) }; html! { - { content } + { if let Some(time) = time { + html! { <> { TimerElapsed(time) } } + } else { + html! { <> { "--:--.--" } } + } } } } diff --git a/src/lib.rs b/src/lib.rs index 9332463..75aec88 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,3 @@ -#![feature(return_position_impl_trait_in_trait)] - pub mod analyzer; pub mod bitset; pub mod server; diff --git a/src/server/local.rs b/src/server/local.rs index fa0b2ab..0d7a191 100644 --- a/src/server/local.rs +++ b/src/server/local.rs @@ -6,8 +6,8 @@ use super::*; use itertools::{chain, izip, repeat_n}; use num::{BigUint, One}; use rand::{distributions::WeightedError, seq::SliceRandom}; +use serde::{Deserialize, Serialize}; use tinyvec::ArrayVec; -use serde::{Serialize, Deserialize}; #[derive(Debug, Clone, Copy, Serialize, Deserialize)] enum Tile { @@ -524,8 +524,8 @@ mod tests { fn win_all_games_with_punishment() { win_all_games(GameConfig { grid_config: GridConfig::expert(), - mode: GameMode::Normal, punish_guessing: true, + ..Default::default() }) } @@ -533,8 +533,8 @@ mod tests { fn win_all_games_without_punishment() { win_all_games(GameConfig { grid_config: GridConfig::expert(), - mode: GameMode::Normal, punish_guessing: false, + ..Default::default() }) } } diff --git a/src/server/mod.rs b/src/server/mod.rs index cd49225..429284b 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -47,6 +47,12 @@ pub struct GridConfig { mine_count: usize, } +impl Default for GridConfig { + fn default() -> Self { + Self::beginner() + } +} + impl fmt::Display for GridConfig { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { #[allow(clippy::match_single_binding)] // false positive @@ -186,8 +192,9 @@ impl GameStatus { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum GameMode { + #[default] Normal, Mindless, Autopilot, @@ -200,6 +207,16 @@ pub struct GameConfig { pub punish_guessing: bool, } +impl Default for GameConfig { + fn default() -> Self { + Self { + grid_config: Default::default(), + mode: Default::default(), + punish_guessing: true, + } + } +} + pub trait Oracle: Serialize + for<'a> Deserialize<'a> + 'static { fn new(config: GameConfig, first_click_id: usize) -> Self;