diff --git a/config.json b/config.json index 276b81e..56d0751 100644 --- a/config.json +++ b/config.json @@ -567,6 +567,14 @@ "prerequisites": [], "difficulty": 6 }, + { + "slug": "go-counting", + "name": "Go Counting", + "uuid": "8c81bf5c-5188-4e1b-9814-1f0f0fcdeec2", + "practices": [], + "prerequisites": [], + "difficulty": 6 + }, { "slug": "gigasecond", "name": "Gigasecond", diff --git a/exercises/practice/go-counting/.docs/instructions.md b/exercises/practice/go-counting/.docs/instructions.md new file mode 100644 index 0000000..e4b143f --- /dev/null +++ b/exercises/practice/go-counting/.docs/instructions.md @@ -0,0 +1,31 @@ +# Instructions + +Count the scored points on a Go board. + +In the game of go (also known as baduk, igo, cờ vây and wéiqí) points are gained by completely encircling empty intersections with your stones. +The encircled intersections of a player are known as its territory. + +Calculate the territory of each player. +You may assume that any stones that have been stranded in enemy territory have already been taken off the board. + +Determine the territory which includes a specified coordinate. + +Multiple empty intersections may be encircled at once and for encircling only horizontal and vertical neighbors count. +In the following diagram the stones which matter are marked "O" and the stones that don't are marked "I" (ignored). +Empty spaces represent empty intersections. + +```text ++----+ +|IOOI| +|O O| +|O OI| +|IOI | ++----+ +``` + +To be more precise an empty intersection is part of a player's territory if all of its neighbors are either stones of that player or empty intersections that are part of that player's territory. + +For more information see [Wikipedia][go-wikipedia] or [Sensei's Library][go-sensei]. + +[go-wikipedia]: https://en.wikipedia.org/wiki/Go_%28game%29 +[go-sensei]: https://senseis.xmp.net/ diff --git a/exercises/practice/go-counting/.meta/Example.roc b/exercises/practice/go-counting/.meta/Example.roc new file mode 100644 index 0000000..8dbd12e --- /dev/null +++ b/exercises/practice/go-counting/.meta/Example.roc @@ -0,0 +1,128 @@ +module [territory, territories] + +Intersection : { x : U64, y : U64 } + +Stone : [White, Black, None] + +Territory : { + owner : Stone, + territory : Set Intersection, +} + +Territories : { + black : Set Intersection, + white : Set Intersection, + none : Set Intersection, +} + +Board : { + rows : List (List Stone), + width : U64, + height : U64, +} + +parse : Str -> Result Board [BoardWasEmpty, BoardWasNotRectangular, InvalidChar U8] +parse = \boardStr -> + if boardStr == "" then + Err BoardWasEmpty + else + + rows = + boardStr + |> Str.split "\n" + |> List.mapTry? \row -> + row + |> Str.toUtf8 + |> List.mapTry \char -> + when char is + 'B' -> Ok Black + 'W' -> Ok White + ' ' -> Ok None + _ -> Err (InvalidChar char) + rowWidths = rows |> List.map List.len + width = rowWidths |> List.max |> Result.withDefault 0 + if rowWidths |> List.any \w -> w != width then + Err BoardWasNotRectangular + else + height = List.len rows + Ok { rows, width, height } + +getStone : Board, Intersection -> Stone +getStone = \board, { x, y } -> + board.rows |> List.get y |> Result.withDefault [] |> List.get x |> Result.withDefault None + +territory : Str, Intersection -> Result Territory [OutOfBounds, BoardWasEmpty, BoardWasNotRectangular, InvalidChar U8] +territory = \boardStr, intersection -> + board = parse? boardStr + if intersection.x >= board.width || intersection.y >= board.height then + Err OutOfBounds + else + + Ok (searchTerritory board intersection) + +searchTerritory : Board, Intersection -> Territory +searchTerritory = \board, intersection -> + help = \toVisit, visited, surroundingStones -> + when toVisit is + [] -> { visited, surroundingStones } + [visiting, .. as restToVisit] -> + if visited |> Set.contains visiting then + help restToVisit visited surroundingStones + else + + stone = board |> getStone visiting + when stone is + Black | White -> + newSurroundingStones = surroundingStones |> Set.insert stone + help restToVisit visited newSurroundingStones + + None -> + neighbors = + [ + { x: visiting.x |> Num.subSaturated 1, y: visiting.y }, + { x: visiting.x + 1, y: visiting.y }, + { x: visiting.x, y: visiting.y |> Num.subSaturated 1 }, + { x: visiting.x, y: visiting.y + 1 }, + ] + |> List.dropIf \neighbor -> + neighbor.x >= board.width || neighbor.y >= board.height || neighbor == visiting + newToVisit = restToVisit |> List.concat neighbors + newVisited = visited |> Set.insert visiting + help newToVisit newVisited surroundingStones + searchResult = help [intersection] (Set.empty {}) (Set.empty {}) + if searchResult.visited |> Set.isEmpty then + { owner: None, territory: Set.empty {} } + else + owner = + if searchResult.surroundingStones == Set.single Black then + Black + else if searchResult.surroundingStones == Set.single White then + White + else + None + { owner, territory: searchResult.visited } + +territories : Str -> Result Territories [BoardWasEmpty, BoardWasNotRectangular, InvalidChar U8] +territories = \boardStr -> + board = parse? boardStr + board.rows + |> List.mapWithIndex \row, y -> + row + |> List.mapWithIndex \stone, x -> + if stone == None then + [{ x, y }] + else + [] + |> List.join + |> List.join + |> List.walk { black: Set.empty {}, white: Set.empty {}, none: Set.empty {} } \state, intersection -> + if state.black |> Set.contains intersection || state.white |> Set.contains intersection || state.none |> Set.contains intersection then + state + else + newTerritory = searchTerritory board intersection + when newTerritory.owner is + Black -> { black: state.black |> Set.union newTerritory.territory, white: state.white, none: state.none } + White -> { black: state.black, white: state.white |> Set.union newTerritory.territory, none: state.none } + None -> { black: state.black, white: state.white, none: state.none |> Set.union newTerritory.territory } + |> Ok + diff --git a/exercises/practice/go-counting/.meta/config.json b/exercises/practice/go-counting/.meta/config.json new file mode 100644 index 0000000..ccb98eb --- /dev/null +++ b/exercises/practice/go-counting/.meta/config.json @@ -0,0 +1,17 @@ +{ + "authors": [ + "ageron" + ], + "files": { + "solution": [ + "GoCounting.roc" + ], + "test": [ + "go-counting-test.roc" + ], + "example": [ + ".meta/Example.roc" + ] + }, + "blurb": "Count the scored points on a Go board." +} diff --git a/exercises/practice/go-counting/.meta/template.j2 b/exercises/practice/go-counting/.meta/template.j2 new file mode 100644 index 0000000..d758478 --- /dev/null +++ b/exercises/practice/go-counting/.meta/template.j2 @@ -0,0 +1,57 @@ +{%- import "generator_macros.j2" as macros with context -%} +{{ macros.canonical_ref() }} +{{ macros.header() }} + +{% macro to_territory(territory) %} +{%- if territory == [] %} +Set.empty {} +{%- else %} +Set.fromList [ +{%- for intersection in territory %} + { x : {{ intersection[0] }}, y : {{ intersection[1] }} }, +{%- endfor %} +] +{%- endif %} +{% endmacro %} + +import {{ exercise | to_pascal }} exposing [territory, territories] + +## The following two comparison functions are temporary workarounds for Roc issue #7144: +## comparing tags or records containing sets sometimes returns the wrong result +## depending on the internal order of the set data, so we have to unwrap the sets +## in order to compare them properly. +compareTerritory = \maybeResult, maybeExpected -> + when (maybeResult, maybeExpected) is + (Ok result, Ok expected) -> result.owner == expected.owner && result.territory == expected.territory + _ -> Bool.false + +compareTerritories = \maybeResult, maybeExpected -> + when (maybeResult, maybeExpected) is + (Ok result, Ok expected) -> result.black == expected.black && result.white == expected.white && result.none == expected.none + _ -> Bool.false + +{% for case in cases -%} +# {{ case["description"] }} +expect + board = {{ case["input"]["board"] | to_roc_multiline_string | replace(" ", "·") | indent(8) }} |> Str.replaceEach "·" " " + result = board |> {{ case["property"] | to_camel }} + {%- if case["property"] == "territory" %} { x : {{ case["input"]["x"] }}, y : {{ case["input"]["y"] }} }{% endif %} + {%- if case["expected"]["error"] %} + result |> Result.isErr + {%- elif case["expected"]["owner"] %} + expected = Ok { + owner: {{ case["expected"]["owner"] | to_pascal }}, + territory: {{ to_territory(case["expected"]["territory"]) }}, + } + result |> compareTerritory expected + {%- else %} + expected = Ok { + black: {{ to_territory(case["expected"]["territoryBlack"]) }}, + white: {{ to_territory(case["expected"]["territoryWhite"]) }}, + none: {{ to_territory(case["expected"]["territoryNone"]) }}, + } + result |> compareTerritories expected + {%- endif %} + + +{% endfor %} diff --git a/exercises/practice/go-counting/.meta/tests.toml b/exercises/practice/go-counting/.meta/tests.toml new file mode 100644 index 0000000..c2e1aa8 --- /dev/null +++ b/exercises/practice/go-counting/.meta/tests.toml @@ -0,0 +1,45 @@ +# 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. + +[94d0c01a-17d0-424c-aab5-2736d0da3939] +description = "Black corner territory on 5x5 board" + +[b33bec54-356a-485c-9c71-1142a9403213] +description = "White center territory on 5x5 board" + +[def7d124-422e-44ae-90e5-ceda09399bda] +description = "Open corner territory on 5x5 board" + +[57d79036-2618-47f4-aa87-56c06d362e3d] +description = "A stone and not a territory on 5x5 board" + +[0c84f852-e032-4762-9010-99f6a001da96] +description = "Invalid because X is too low for 5x5 board" +include = false + +[6f867945-9b2c-4bdd-b23e-b55fe2069a68] +description = "Invalid because X is too high for 5x5 board" + +[d67aaffd-fdf1-4e7f-b9e9-79897402b64a] +description = "Invalid because Y is too low for 5x5 board" +include = false + +[14f23c25-799e-4371-b3e5-777a2c30357a] +description = "Invalid because Y is too high for 5x5 board" + +[37fb04b5-98c1-4b96-8c16-af2d13624afd] +description = "One territory is the whole board" + +[9a1c59b7-234b-495a-8d60-638489f0fc0a] +description = "Two territory rectangular board" + +[d1645953-1cd5-4221-af6f-8164f96249e1] +description = "Two region rectangular board" diff --git a/exercises/practice/go-counting/GoCounting.roc b/exercises/practice/go-counting/GoCounting.roc new file mode 100644 index 0000000..8a891cb --- /dev/null +++ b/exercises/practice/go-counting/GoCounting.roc @@ -0,0 +1,24 @@ +module [territory, territories] + +Intersection : { x : U64, y : U64 } + +Stone : [White, Black, None] + +Territory : { + owner : Stone, + territory : Set Intersection, +} + +Territories : { + black : Set Intersection, + white : Set Intersection, + none : Set Intersection, +} + +territory : Str, Intersection -> Result Territory _ +territory = \boardStr, { x, y } -> + crash "Please implement the 'territory' function" + +territories : Str -> Result Territories _ +territories = \boardStr -> + crash "Please implement the 'territories' function" diff --git a/exercises/practice/go-counting/go-counting-test.roc b/exercises/practice/go-counting/go-counting-test.roc new file mode 100644 index 0000000..c994772 --- /dev/null +++ b/exercises/practice/go-counting/go-counting-test.roc @@ -0,0 +1,185 @@ +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/go-counting/canonical-data.json +# File last updated on 2024-10-07 +app [main] { + pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.15.0/SlwdbJ-3GR7uBWQo6zlmYWNYOxnvo8r6YABXD-45UOw.tar.br", +} + +main = + Task.ok {} + +import GoCounting exposing [territory, territories] + +## The following two comparison functions are temporary workarounds for Roc issue #7144: +## comparing tags or records containing sets sometimes returns the wrong result +## depending on the internal order of the set data, so we have to unwrap the sets +## in order to compare them properly. +compareTerritory = \maybeResult, maybeExpected -> + when (maybeResult, maybeExpected) is + (Ok result, Ok expected) -> result.owner == expected.owner && result.territory == expected.territory + _ -> Bool.false + +compareTerritories = \maybeResult, maybeExpected -> + when (maybeResult, maybeExpected) is + (Ok result, Ok expected) -> result.black == expected.black && result.white == expected.white && result.none == expected.none + _ -> Bool.false + +# Black corner territory on 5x5 board +expect + board = + """ + ··B·· + ·B·B· + B·W·B + ·W·W· + ··W·· + """ + |> Str.replaceEach "·" " " + result = board |> territory { x: 0, y: 1 } + expected = Ok { + owner: Black, + territory: Set.fromList [ + { x: 0, y: 0 }, + { x: 0, y: 1 }, + { x: 1, y: 0 }, + ], + } + result |> compareTerritory expected + +# White center territory on 5x5 board +expect + board = + """ + ··B·· + ·B·B· + B·W·B + ·W·W· + ··W·· + """ + |> Str.replaceEach "·" " " + result = board |> territory { x: 2, y: 3 } + expected = Ok { + owner: White, + territory: Set.fromList [ + { x: 2, y: 3 }, + ], + } + result |> compareTerritory expected + +# Open corner territory on 5x5 board +expect + board = + """ + ··B·· + ·B·B· + B·W·B + ·W·W· + ··W·· + """ + |> Str.replaceEach "·" " " + result = board |> territory { x: 1, y: 4 } + expected = Ok { + owner: None, + territory: Set.fromList [ + { x: 0, y: 3 }, + { x: 0, y: 4 }, + { x: 1, y: 4 }, + ], + } + result |> compareTerritory expected + +# A stone and not a territory on 5x5 board +expect + board = + """ + ··B·· + ·B·B· + B·W·B + ·W·W· + ··W·· + """ + |> Str.replaceEach "·" " " + result = board |> territory { x: 1, y: 1 } + expected = Ok { + owner: None, + territory: Set.empty {}, + } + result |> compareTerritory expected + +# Invalid because X is too high for 5x5 board +expect + board = + """ + ··B·· + ·B·B· + B·W·B + ·W·W· + ··W·· + """ + |> Str.replaceEach "·" " " + result = board |> territory { x: 5, y: 1 } + result |> Result.isErr + +# Invalid because Y is too high for 5x5 board +expect + board = + """ + ··B·· + ·B·B· + B·W·B + ·W·W· + ··W·· + """ + |> Str.replaceEach "·" " " + result = board |> territory { x: 1, y: 5 } + result |> Result.isErr + +# One territory is the whole board +expect + board = "·" |> Str.replaceEach "·" " " + result = board |> territories + expected = Ok { + black: Set.empty {}, + white: Set.empty {}, + none: Set.fromList [ + { x: 0, y: 0 }, + ], + } + result |> compareTerritories expected + +# Two territory rectangular board +expect + board = + """ + ·BW· + ·BW· + """ + |> Str.replaceEach "·" " " + result = board |> territories + expected = Ok { + black: Set.fromList [ + { x: 0, y: 0 }, + { x: 0, y: 1 }, + ], + white: Set.fromList [ + { x: 3, y: 0 }, + { x: 3, y: 1 }, + ], + none: Set.empty {}, + } + result |> compareTerritories expected + +# Two region rectangular board +expect + board = "·B·" |> Str.replaceEach "·" " " + result = board |> territories + expected = Ok { + black: Set.fromList [ + { x: 0, y: 0 }, + { x: 2, y: 0 }, + ], + white: Set.empty {}, + none: Set.empty {}, + } + result |> compareTerritories expected +