Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add go-counting exercise #134

Merged
merged 1 commit into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
31 changes: 31 additions & 0 deletions exercises/practice/go-counting/.docs/instructions.md
Original file line number Diff line number Diff line change
@@ -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/
128 changes: 128 additions & 0 deletions exercises/practice/go-counting/.meta/Example.roc
Original file line number Diff line number Diff line change
@@ -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

17 changes: 17 additions & 0 deletions exercises/practice/go-counting/.meta/config.json
Original file line number Diff line number Diff line change
@@ -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."
}
57 changes: 57 additions & 0 deletions exercises/practice/go-counting/.meta/template.j2
Original file line number Diff line number Diff line change
@@ -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 %}
45 changes: 45 additions & 0 deletions exercises/practice/go-counting/.meta/tests.toml
Original file line number Diff line number Diff line change
@@ -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"
24 changes: 24 additions & 0 deletions exercises/practice/go-counting/GoCounting.roc
Original file line number Diff line number Diff line change
@@ -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"
Loading