diff --git a/config.json b/config.json index db6c3faab2..8506065253 100644 --- a/config.json +++ b/config.json @@ -1042,6 +1042,19 @@ "loops" ] }, + { + "uuid": "d4ddeb18-ac22-11e7-abc4-cec278b6b50a", + "slug": "go-counting", + "core": false, + "unlocked_by": null, + "difficulty": 4, + "topics": [ + "parsing", + "tuples", + "optional_values", + "classes" + ] + }, { "uuid": "7f4d5743-7ab8-42ca-b393-767f7e9a4e97", "slug": "complex-numbers", diff --git a/exercises/go-counting/README.md b/exercises/go-counting/README.md new file mode 100644 index 0000000000..0d21809476 --- /dev/null +++ b/exercises/go-counting/README.md @@ -0,0 +1,44 @@ +# Go Counting + +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. + +Write a function that determines 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. + +Multiple empty intersections may be encircled at once and for encircling +only horizontal and vertical neighbours 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. + +``` ++----+ +|IOOI| +|O O| +|O OI| +|IOI | ++----+ +``` + +To be more precise an empty intersection is part of a player's territory +if all of its neighbours are either stones of that player or empty +intersections that are part of that player's territory. + +For more information see +[wikipedia](https://en.wikipedia.org/wiki/Go_%28game%29) or [Sensei's +Library](http://senseis.xmp.net/). + +## Submitting Exercises + +Note that, when trying to submit an exercise, make sure the solution is in the `exercism/python/` directory. + +For example, if you're submitting `bob.py` for the Bob exercise, the submit command would be something like `exercism submit /python/bob/bob.py`. + +## Submitting Incomplete Solutions +It's possible to submit an incomplete solution so you can see how others have completed the +exercise. diff --git a/exercises/go-counting/example.py b/exercises/go-counting/example.py new file mode 100644 index 0000000000..b0ba0ee0ba --- /dev/null +++ b/exercises/go-counting/example.py @@ -0,0 +1,62 @@ + +BLACK = "B" +WHITE = "W" +NONE = "" +STONES = [BLACK, WHITE] +DIRECTIONS = [(0, 1), (0, -1), (1, 0), (-1, 0)] + + +class Board: + def __init__(self, board): + self.board = board.splitlines() + self.width = len(self.board[0]) + self.height = len(self.board) + + def valid(self, x, y): + return x >= 0 and x < self.width and y >= 0 and y < self.height + + def walk(self, x, y, + visited_territory=[], + visited_coords=[], + visited_stones=[]): + if not (x, y) in visited_coords and self.valid(x, y): + s = self.board[y][x] + if s in STONES: + if s not in visited_stones: + return (visited_territory, visited_stones + [s]) + else: # s is empty + for d in DIRECTIONS: + visited = self.walk(x + d[0], y + d[1], + visited_territory + [(x, y)], + visited_coords + [(x, y)], + visited_stones) + visited_territory = visited[0] + visited_stones = visited[1] + + return (visited_territory, visited_stones) + + def territoryFor(self, coord): + assert len(coord) == 2 + x, y = coord[0], coord[1] + if not self.valid(x, y) or self.board[y][x] in STONES: + return (NONE, set()) + + visited_territory, visited_stones = self.walk(x, y) + result = set(visited_territory) + + if len(visited_stones) == 1: + return (visited_stones[0], result) + return (NONE, result) + + def territories(self): + owners = STONES + [NONE] + result = dict([(owner, set()) for owner in owners]) + visited = set() + for y in range(self.height): + for x in range(self.width): + if not (x, y) in visited: + owner, owned_territories = self.territoryFor((x, y)) + result[owner].update(owned_territories) + visited.update(owned_territories) + + return result diff --git a/exercises/go-counting/go_counting.py b/exercises/go-counting/go_counting.py new file mode 100644 index 0000000000..a35bf6f7b3 --- /dev/null +++ b/exercises/go-counting/go_counting.py @@ -0,0 +1,38 @@ + +class Board: + """Count territories of each player in a Go game + + Args: + board (list[str]): A two-dimensional Go board + """ + + def __init__(self, board): + pass + + def territoryFor(self, coord): + """Find the owner and the territories given a coordinate on + the board + + Args: + coord ((int,int)): Coordinate on the board + + Returns: + (str, set): A tuple, the first element being the owner + of that area. One of "W", "B", "". The + second being a set of coordinates, representing + the owner's territories. + """ + pass + + def territories(self): + """Find the owners and the territories of the whole board + + Args: + none + + Returns: + dict(str, set): A dictionary whose key being the owner + , i.e. "W", "B", "". The value being a set + of coordinates owned by the owner. + """ + pass diff --git a/exercises/go-counting/go_counting_test.py b/exercises/go-counting/go_counting_test.py new file mode 100644 index 0000000000..207eeaf407 --- /dev/null +++ b/exercises/go-counting/go_counting_test.py @@ -0,0 +1,92 @@ +import unittest +import gocounting + + +# Tests adapted from `problem-specifications//canonical-data.json` @ v1.1.0 + +board5x5 = "\n".join([ + " B ", + " B B ", + "B W B", + " W W ", + " W " +]) + +board9x9 = "\n".join([ + " B B ", + "B B B", + "WBBBWBBBW", + "W W W W W", + " ", + " W W W W ", + "B B B B", + " W BBB W ", + " B B " +]) + + +class GoCountingTest(unittest.TestCase): + def test_5x5_for_black(self): + board = gocounting.Board(board5x5) + stone, territory = board.territoryFor((0, 1)) + self.assertEqual(stone, gocounting.BLACK) + self.assertEqual(territory, set([(0, 0), (0, 1), (1, 0)])) + + def test_5x5_for_white(self): + board = gocounting.Board(board5x5) + stone, territory = board.territoryFor((2, 3)) + self.assertEqual(stone, gocounting.WHITE) + self.assertEqual(territory, set([(2, 3)])) + + def test_5x5_for_open_territory(self): + board = gocounting.Board(board5x5) + stone, territory = board.territoryFor((1, 4)) + self.assertEqual(stone, gocounting.NONE) + self.assertEqual(territory, set([(0, 3), (0, 4), (1, 4)])) + + def test_5x5_for_non_territory(self): + board = gocounting.Board(board5x5) + stone, territory = board.territoryFor((1, 1)) + self.assertEqual(stone, gocounting.NONE) + self.assertEqual(territory, set()) + + def test_5x5_for_valid_coordinate(self): + board = gocounting.Board(board5x5) + stone, territory = board.territoryFor((-1, 1)) + self.assertEqual(stone, gocounting.NONE) + self.assertEqual(territory, set()) + + def test_5x5_for_valid_coordinate2(self): + board = gocounting.Board(board5x5) + stone, territory = board.territoryFor((1, 5)) + self.assertEqual(stone, gocounting.NONE) + self.assertEqual(territory, set()) + + def test_one_territory_whole_board(self): + board = gocounting.Board(" ") + territories = board.territories() + self.assertEqual(territories[gocounting.BLACK], set()) + self.assertEqual(territories[gocounting.WHITE], set()) + self.assertEqual(territories[gocounting.NONE], set([(0, 0)])) + + def test_two_territories_rectangular_board(self): + input_board = "\n".join([ + " BW ", + " BW " + ]) + board = gocounting.Board(input_board) + territories = board.territories() + self.assertEqual(territories[gocounting.BLACK], set([(0, 0), (0, 1)])) + self.assertEqual(territories[gocounting.WHITE], set([(3, 0), (3, 1)])) + self.assertEqual(territories[gocounting.NONE], set()) + + def test_9x9_for_open_territory(self): + board = gocounting.Board(board9x9) + stone, territory = board.territoryFor((0, 8)) + self.assertEqual(stone, gocounting.NONE) + self.assertEqual(territory, + set([(2, 7), (2, 8), (1, 8), (0, 8), (0, 7)])) + + +if __name__ == '__main__': + unittest.main()