From eee9364f48fdae523448f86bc2849a9cacf99f15 Mon Sep 17 00:00:00 2001 From: Eric Willigers Date: Sat, 13 Jul 2024 20:15:57 +1000 Subject: [PATCH] Add minesweeper exercise --- config.json | 8 + .../minesweeper/.docs/instructions.append.md | 12 + .../minesweeper/.docs/instructions.md | 26 ++ .../minesweeper/.docs/introduction.md | 5 + exercises/practice/minesweeper/.eslintrc | 18 ++ .../practice/minesweeper/.meta/config.json | 26 ++ .../practice/minesweeper/.meta/proof.ci.wat | 116 ++++++++ .../practice/minesweeper/.meta/tests.toml | 46 ++++ exercises/practice/minesweeper/.npmrc | 1 + exercises/practice/minesweeper/LICENSE | 21 ++ .../practice/minesweeper/babel.config.js | 4 + .../practice/minesweeper/minesweeper.spec.js | 259 ++++++++++++++++++ .../practice/minesweeper/minesweeper.wat | 15 + exercises/practice/minesweeper/package.json | 34 +++ 14 files changed, 591 insertions(+) create mode 100644 exercises/practice/minesweeper/.docs/instructions.append.md create mode 100644 exercises/practice/minesweeper/.docs/instructions.md create mode 100644 exercises/practice/minesweeper/.docs/introduction.md create mode 100644 exercises/practice/minesweeper/.eslintrc create mode 100644 exercises/practice/minesweeper/.meta/config.json create mode 100644 exercises/practice/minesweeper/.meta/proof.ci.wat create mode 100644 exercises/practice/minesweeper/.meta/tests.toml create mode 100644 exercises/practice/minesweeper/.npmrc create mode 100644 exercises/practice/minesweeper/LICENSE create mode 100644 exercises/practice/minesweeper/babel.config.js create mode 100644 exercises/practice/minesweeper/minesweeper.spec.js create mode 100644 exercises/practice/minesweeper/minesweeper.wat create mode 100644 exercises/practice/minesweeper/package.json diff --git a/config.json b/config.json index 8b3a299..559267b 100644 --- a/config.json +++ b/config.json @@ -239,6 +239,14 @@ "prerequisites": [], "difficulty": 6 }, + { + "slug": "minesweeper", + "name": "Minesweeper", + "uuid": "8db919e4-52a1-44e4-b93a-02df342a4396", + "practices": [], + "prerequisites": [], + "difficulty": 8 + }, { "slug": "run-length-encoding", "name": "Run-Length Encoding", diff --git a/exercises/practice/minesweeper/.docs/instructions.append.md b/exercises/practice/minesweeper/.docs/instructions.append.md new file mode 100644 index 0000000..91a7969 --- /dev/null +++ b/exercises/practice/minesweeper/.docs/instructions.append.md @@ -0,0 +1,12 @@ + +## Minefield format + +The minefield is represented as a string, with a newline character at the end of each row. + +An example would be `" \n * \n \n"` + +## Reserved Memory + +The buffer for the input string uses bytes 64-319 of linear memory. + +The input string can be modified in place if desired. diff --git a/exercises/practice/minesweeper/.docs/instructions.md b/exercises/practice/minesweeper/.docs/instructions.md new file mode 100644 index 0000000..7c1df2e --- /dev/null +++ b/exercises/practice/minesweeper/.docs/instructions.md @@ -0,0 +1,26 @@ +# Instructions + +Your task is to add the mine counts to empty squares in a completed Minesweeper board. +The board itself is a rectangle composed of squares that are either empty (`' '`) or a mine (`'*'`). + +For each empty square, count the number of mines adjacent to it (horizontally, vertically, diagonally). +If the empty square has no adjacent mines, leave it empty. +Otherwise replace it with the adjacent mines count. + +For example, you may receive a 5 x 4 board like this (empty spaces are represented here with the '·' character for display on screen): + +```text +·*·*· +··*·· +··*·· +····· +``` + +Which your code should transform into this: + +```text +1*3*1 +13*31 +·2*2· +·111· +``` diff --git a/exercises/practice/minesweeper/.docs/introduction.md b/exercises/practice/minesweeper/.docs/introduction.md new file mode 100644 index 0000000..5f74a74 --- /dev/null +++ b/exercises/practice/minesweeper/.docs/introduction.md @@ -0,0 +1,5 @@ +# Introduction + +[Minesweeper][wikipedia] is a popular game where the user has to find the mines using numeric hints that indicate how many mines are directly adjacent (horizontally, vertically, diagonally) to a square. + +[wikipedia]: https://en.wikipedia.org/wiki/Minesweeper_(video_game) diff --git a/exercises/practice/minesweeper/.eslintrc b/exercises/practice/minesweeper/.eslintrc new file mode 100644 index 0000000..1dbeac2 --- /dev/null +++ b/exercises/practice/minesweeper/.eslintrc @@ -0,0 +1,18 @@ +{ + "root": true, + "extends": "@exercism/eslint-config-javascript", + "env": { + "jest": true + }, + "overrides": [ + { + "files": [ + "*.spec.js" + ], + "excludedFiles": [ + "custom.spec.js" + ], + "extends": "@exercism/eslint-config-javascript/maintainers" + } + ] +} diff --git a/exercises/practice/minesweeper/.meta/config.json b/exercises/practice/minesweeper/.meta/config.json new file mode 100644 index 0000000..d7dfecb --- /dev/null +++ b/exercises/practice/minesweeper/.meta/config.json @@ -0,0 +1,26 @@ +{ + "authors": [ + "keiravillekode" + ], + "files": { + "solution": [ + "minesweeper.wat" + ], + "test": [ + "minesweeper.spec.js" + ], + "example": [ + ".meta/proof.ci.wat" + ], + "invalidator": [ + "package.json" + ] + }, + "blurb": "Add the numbers to a minesweeper board.", + "custom": { + "version.tests.compatibility": "jest-27", + "flag.tests.task-per-describe": false, + "flag.tests.may-run-long": false, + "flag.tests.includes-optional": false + } +} diff --git a/exercises/practice/minesweeper/.meta/proof.ci.wat b/exercises/practice/minesweeper/.meta/proof.ci.wat new file mode 100644 index 0000000..e6762fa --- /dev/null +++ b/exercises/practice/minesweeper/.meta/proof.ci.wat @@ -0,0 +1,116 @@ +(module + (memory (export "mem") 1) + + (global $NEWLINE i32 (i32.const 10)) + + (global $MINE i32 (i32.const 42)) + + (global $ZERO i32 (i32.const 48)) + + ;; + ;; Adds numbers to a minesweeper board. + ;; + ;; @param {i32} offset - The offset of the input string in linear memory + ;; @param {i32} length - The length of the input string in linear memory + ;; + ;; @returns {(i32,i32)} - The offset and length of the output string in linear memory + ;; + (func (export "annotate") (param $offset i32) (param $length i32) (result i32 i32) + + (local $stop i32) + (local $lineLength i32) ;; including newline character + (local $index i32) + (local $char i32) + (local $mineCount i32) + + (local $previousRow i32) + (local $currentRow i32) + (local $nextRow i32) + (local $adjacentRow i32) + + (local $previousColumn i32) + (local $currentColumn i32) + (local $nextColumn i32) + (local $adjacentColumn i32) + + (if (i32.eq (local.get $length) (i32.const 0)) (then + ;; no rows + (return (local.get $offset) (local.get $length)) + )) + + (local.set $stop (i32.add (local.get $offset) (local.get $length))) + (local.set $index (local.get $offset)) + (loop + (local.set $char (i32.load8_u (local.get $index))) + (local.set $index (i32.add (local.get $index) (i32.const 1))) + (br_if 0 (i32.ne (local.get $char) (global.get $NEWLINE))) + ) + (local.set $lineLength (i32.sub (local.get $index) (local.get $offset))) + (if (i32.eq (local.get $lineLength) (i32.const 1)) (then + ;; each row has newline only + (return (local.get $offset) (local.get $length)) + )) + + (local.set $currentRow (local.get $offset)) + (local.set $nextRow (local.get $offset)) ;; start of first row + + (loop $eachCurrentRow + (local.set $previousRow (local.get $currentRow)) ;; current row becomes previous row + (local.set $currentRow (local.get $nextRow)) ;; next row becomes current row + (local.set $nextRow (i32.add (local.get $currentRow) (local.get $lineLength))) + (if (i32.eq (local.get $nextRow) (local.get $stop)) (then + (local.set $nextRow (local.get $currentRow)) ;; last row + )) + + (local.set $currentColumn (i32.const 0)) + (local.set $nextColumn (i32.const 0)) ;; first column + + (loop $eachCurrentColumn + (local.set $previousColumn (local.get $currentColumn)) ;; current column becomes previous column + (local.set $currentColumn (local.get $nextColumn)) ;; next column becomes current column + (if (i32.ne (i32.add (local.get $nextColumn) (i32.const 2)) + (local.get $lineLength)) (then + (local.set $nextColumn (i32.add (local.get $currentColumn) (i32.const 1))) + )) + + (local.set $index (i32.add (local.get $currentRow) (local.get $currentColumn))) ;; address of minefield square + (local.set $char (i32.load8_u (local.get $index))) + (if (i32.ne (local.get $char) (global.get $MINE)) (then + (local.set $mineCount (i32.const 0)) ;; number of adjacent mines + + (local.set $adjacentRow (i32.sub (local.get $previousRow) (local.get $lineLength))) + (loop $eachAdjacentRow + ;; address of adjacent row: $previousRow, $previousRow+$lineLength, $nextRow + (local.set $adjacentRow (i32.add (local.get $adjacentRow) (local.get $lineLength))) + + (local.set $adjacentColumn (i32.sub (local.get $previousColumn) (i32.const 1))) + (loop $eachAdjacentColumn + ;; index of adjacent column in row: $previousColumn, $previousColumn+1, $nextColumn + (local.set $adjacentColumn (i32.add (local.get $adjacentColumn) (i32.const 1))) + + ;; content of adjacent square + (local.set $char (i32.load8_u (i32.add (local.get $adjacentRow) + (local.get $adjacentColumn)))) + ;; increment mineCount if square contains mine + (local.set $mineCount (i32.add (local.get $mineCount) + (i32.eq (local.get $char) (global.get $MINE)))) + (br_if $eachAdjacentColumn (i32.ne (local.get $adjacentColumn) (local.get $nextColumn))) + ) + + (br_if $eachAdjacentRow (i32.ne (local.get $adjacentRow) (local.get $nextRow))) + ) + + (if (local.get $mineCount) (then + (i32.store8 (local.get $index) (i32.add (global.get $ZERO) (local.get $mineCount))) + )) + )) + + (br_if $eachCurrentColumn (i32.ne (local.get $currentColumn) (local.get $nextColumn))) + ) + + (br_if $eachCurrentRow (i32.ne (local.get $currentRow) (local.get $nextRow))) + ) + + (return (local.get $offset) (local.get $length)) + ) +) diff --git a/exercises/practice/minesweeper/.meta/tests.toml b/exercises/practice/minesweeper/.meta/tests.toml new file mode 100644 index 0000000..2a14222 --- /dev/null +++ b/exercises/practice/minesweeper/.meta/tests.toml @@ -0,0 +1,46 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[0c5ec4bd-dea7-4138-8651-1203e1cb9f44] +description = "no rows" + +[650ac4c0-ad6b-4b41-acde-e4ea5852c3b8] +description = "no columns" + +[6fbf8f6d-a03b-42c9-9a58-b489e9235478] +description = "no mines" + +[61aff1c4-fb31-4078-acad-cd5f1e635655] +description = "minefield with only mines" + +[84167147-c504-4896-85d7-246b01dea7c5] +description = "mine surrounded by spaces" + +[cb878f35-43e3-4c9d-93d9-139012cccc4a] +description = "space surrounded by mines" + +[7037f483-ddb4-4b35-b005-0d0f4ef4606f] +description = "horizontal line" + +[e359820f-bb8b-4eda-8762-47b64dba30a6] +description = "horizontal line, mines at edges" + +[c5198b50-804f-47e9-ae02-c3b42f7ce3ab] +description = "vertical line" + +[0c79a64d-703d-4660-9e90-5adfa5408939] +description = "vertical line, mines at edges" + +[4b098563-b7f3-401c-97c6-79dd1b708f34] +description = "cross" + +[04a260f1-b40a-4e89-839e-8dd8525abe0e] +description = "large minefield" diff --git a/exercises/practice/minesweeper/.npmrc b/exercises/practice/minesweeper/.npmrc new file mode 100644 index 0000000..d26df80 --- /dev/null +++ b/exercises/practice/minesweeper/.npmrc @@ -0,0 +1 @@ +audit=false diff --git a/exercises/practice/minesweeper/LICENSE b/exercises/practice/minesweeper/LICENSE new file mode 100644 index 0000000..90e73be --- /dev/null +++ b/exercises/practice/minesweeper/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Exercism + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/exercises/practice/minesweeper/babel.config.js b/exercises/practice/minesweeper/babel.config.js new file mode 100644 index 0000000..9c17ba5 --- /dev/null +++ b/exercises/practice/minesweeper/babel.config.js @@ -0,0 +1,4 @@ +export default { + presets: ["@exercism/babel-preset-javascript"], + plugins: [], +}; diff --git a/exercises/practice/minesweeper/minesweeper.spec.js b/exercises/practice/minesweeper/minesweeper.spec.js new file mode 100644 index 0000000..a276885 --- /dev/null +++ b/exercises/practice/minesweeper/minesweeper.spec.js @@ -0,0 +1,259 @@ +import { compileWat, WasmRunner } from "@exercism/wasm-lib"; + +let wasmModule; +let currentInstance; + +beforeAll(async () => { + try { + const watPath = new URL("./minesweeper.wat", import.meta.url); + const { buffer } = await compileWat(watPath); + wasmModule = await WebAssembly.compile(buffer); + } catch (err) { + console.log(`Error compiling *.wat: \n${err}`); + process.exit(1); + } +}); + +function annotate(input) { + const inputBufferOffset = 64; + const inputBufferCapacity = 256; + + const inputLengthEncoded = new TextEncoder().encode(input).length; + if (inputLengthEncoded > inputBufferCapacity) { + throw new Error( + `String is too large for buffer of size ${inputBufferCapacity} bytes` + ); + } + + currentInstance.set_mem_as_utf8(inputBufferOffset, inputLengthEncoded, input); + + // Pass offset and length to WebAssembly function + const [outputOffset, outputLength] = currentInstance.exports.annotate( + inputBufferOffset, + inputLengthEncoded + ); + expect(outputLength).toEqual(inputLengthEncoded); + + // Decode JS string from returned offset and length + return currentInstance.get_mem_as_utf8(outputOffset, outputLength); +} + +describe("annotate()", () => { + beforeEach(async () => { + currentInstance = null; + if (!wasmModule) { + return Promise.reject(); + } + try { + currentInstance = await new WasmRunner(wasmModule); + return Promise.resolve(); + } catch (err) { + console.log(`Error instantiating WebAssembly module: ${err}`); + return Promise.reject(); + } + }); + + test("no rows", () => { + const minefield = [ + "" + ].join("\n"); + const expected = [ + "" + ].join("\n"); + const actual = annotate(minefield); + expect(actual).toEqual(expected); + }); + + xtest("no columns", () => { + const minefield = [ + "", + "" + ].join("\n"); + const expected = [ + "", + "" + ].join("\n"); + const actual = annotate(minefield); + expect(actual).toEqual(expected); + }); + + xtest("no mines", () => { + const minefield = [ + " ", + " ", + " ", + "" + ].join("\n"); + const expected = [ + " ", + " ", + " ", + "" + ].join("\n"); + const actual = annotate(minefield); + expect(actual).toEqual(expected); + }); + + xtest("minefield with only mines", () => { + const minefield = [ + "***", + "***", + "***", + "" + ].join("\n"); + const expected = [ + "***", + "***", + "***", + "" + ].join("\n"); + const actual = annotate(minefield); + expect(actual).toEqual(expected); + }); + + xtest("mine surrounded by spaces", () => { + const minefield = [ + " ", + " * ", + " ", + "" + ].join("\n"); + const expected = [ + "111", + "1*1", + "111", + "" + ].join("\n"); + const actual = annotate(minefield); + expect(actual).toEqual(expected); + }); + + xtest("space surrounded by mines", () => { + const minefield = [ + "***", + "* *", + "***", + "" + ].join("\n"); + const expected = [ + "***", + "*8*", + "***", + "" + ].join("\n"); + const actual = annotate(minefield); + expect(actual).toEqual(expected); + }); + + xtest("horizontal line", () => { + const minefield = [ + " * * ", + "" + ].join("\n"); + const expected = [ + "1*2*1", + "" + ].join("\n"); + const actual = annotate(minefield); + expect(actual).toEqual(expected); + }); + + xtest("horizontal line, mines at edges", () => { + const minefield = [ + "* *", + "" + ].join("\n"); + const expected = [ + "*1 1*", + "" + ].join("\n"); + const actual = annotate(minefield); + expect(actual).toEqual(expected); + }); + + xtest("vertical line", () => { + const minefield = [ + " ", + "*", + " ", + "*", + " ", + "" + ].join("\n"); + const expected = [ + "1", + "*", + "2", + "*", + "1", + "" + ].join("\n"); + const actual = annotate(minefield); + expect(actual).toEqual(expected); + }); + + xtest("vertical line, mines at edges", () => { + const minefield = [ + "*", + " ", + " ", + " ", + "*", + "" + ].join("\n"); + const expected = [ + "*", + "1", + " ", + "1", + "*", + "" + ].join("\n"); + const actual = annotate(minefield); + expect(actual).toEqual(expected); + }); + + xtest("cross", () => { + const minefield = [ + " * ", + " * ", + "*****", + " * ", + " * ", + "" + ].join("\n"); + const expected = [ + " 2*2 ", + "25*52", + "*****", + "25*52", + " 2*2 ", + "" + ].join("\n"); + const actual = annotate(minefield); + expect(actual).toEqual(expected); + }); + + xtest("large minefield", () => { + const minefield = [ + " * * ", + " * ", + " * ", + " * *", + " * * ", + " ", + "" + ].join("\n"); + const expected = [ + "1*22*1", + "12*322", + " 123*2", + "112*4*", + "1*22*2", + "111111", + "" + ].join("\n"); + const actual = annotate(minefield); + expect(actual).toEqual(expected); + }); +}); diff --git a/exercises/practice/minesweeper/minesweeper.wat b/exercises/practice/minesweeper/minesweeper.wat new file mode 100644 index 0000000..fdc2d32 --- /dev/null +++ b/exercises/practice/minesweeper/minesweeper.wat @@ -0,0 +1,15 @@ +(module + (memory (export "mem") 1) + + ;; + ;; Adds numbers to a minesweeper board. + ;; + ;; @param {i32} offset - The offset of the input string in linear memory + ;; @param {i32} length - The length of the input string in linear memory + ;; + ;; @returns {(i32,i32)} - The offset and length of the output string in linear memory + ;; + (func (export "annotate") (param $offset i32) (param $length i32) (result i32 i32) + (return (local.get $offset) (local.get $length)) + ) +) diff --git a/exercises/practice/minesweeper/package.json b/exercises/practice/minesweeper/package.json new file mode 100644 index 0000000..a706c6d --- /dev/null +++ b/exercises/practice/minesweeper/package.json @@ -0,0 +1,34 @@ +{ + "name": "@exercism/wasm-minesweeper", + "description": "Exercism exercises in WebAssembly.", + "type": "module", + "private": true, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/exercism/wasm", + "directory": "exercises/practice/minesweeper" + }, + "jest": { + "maxWorkers": 1 + }, + "devDependencies": { + "@babel/core": "^7.23.3", + "@exercism/babel-preset-javascript": "^0.4.0", + "@exercism/eslint-config-javascript": "^0.6.0", + "@types/jest": "^29.5.8", + "@types/node": "^20.9.1", + "babel-jest": "^29.7.0", + "core-js": "^3.33.2", + "eslint": "^8.54.0", + "jest": "^29.7.0" + }, + "dependencies": { + "@exercism/wasm-lib": "^0.2.0" + }, + "scripts": { + "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js ./*", + "watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch ./*", + "lint": "eslint ." + } +}