Browse Source

Initial commit

master
Dylan Baker 11 months ago
commit
248d1aab1d
7 changed files with 501 additions and 0 deletions
  1. 1
    0
      .gitignore
  2. 19
    0
      LICENSE
  3. 38
    0
      README.md
  4. 123
    0
      conway.py
  5. 152
    0
      poetry.lock
  6. 15
    0
      pyproject.toml
  7. 153
    0
      test_conway.py

+ 1
- 0
.gitignore View File

@@ -0,0 +1 @@
1
+__pycache__

+ 19
- 0
LICENSE View File

@@ -0,0 +1,19 @@
1
+Copyright (c) 2020 Dylan Baker
2
+
3
+Permission is hereby granted, free of charge, to any person obtaining a copy
4
+of this software and associated documentation files (the "Software"), to deal
5
+in the Software without restriction, including without limitation the rights
6
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+copies of the Software, and to permit persons to whom the Software is
8
+furnished to do so, subject to the following conditions:
9
+
10
+The above copyright notice and this permission notice shall be included in all
11
+copies or substantial portions of the Software.
12
+
13
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+SOFTWARE.

+ 38
- 0
README.md View File

@@ -0,0 +1,38 @@
1
+# Conway's Game of Life
2
+
3
+An command-line implementation of [Conway's Game of
4
+Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) in Python. It looks
5
+better with a square font such as [Square](http://strlen.com/square/).
6
+
7
+## Usage
8
+
9
+```
10
+$ python conway.py --help
11
+usage: conway.py [-h] [-f FILE] [-s SPEED]
12
+
13
+Conway's Game of Life
14
+
15
+optional arguments:
16
+
17
+  -h, --help
18
+
19
+     show this help message and exit
20
+
21
+  -f FILE, --file FILE
22
+
23
+     The path to a file containing a starting grid. The grid file should use 0 for dead cells and 1 for living cells.
24
+
25
+  -s SPEED, --speed SPEED
26
+
27
+     The delay between iterations in milliseconds (default: 500, minimum: 100)
28
+```
29
+
30
+## Testing
31
+
32
+```
33
+$ pytest
34
+```
35
+
36
+## License
37
+
38
+[MIT](https://opensource.org/licenses/MIT)

+ 123
- 0
conway.py View File

@@ -0,0 +1,123 @@
1
+import argparse
2
+import copy
3
+import os
4
+import sys
5
+import time
6
+
7
+DEFAULT_GRID = [
8
+    [0, 0, 0, 0, 0],
9
+    [0, 0, 1, 0, 0],
10
+    [0, 0, 1, 0, 0],
11
+    [0, 0, 1, 0, 0],
12
+    [0, 0, 0, 0, 0],
13
+]
14
+
15
+DEFAULT_SPEED = 500
16
+
17
+
18
+class GameOfLife:
19
+    def __init__(self, speed: int = DEFAULT_SPEED, grid: list[list[int]] = DEFAULT_GRID):
20
+        self.grid = grid.copy()
21
+        self.height = len(self.grid)
22
+        self.width = len(self.grid[0])
23
+        self.speed = speed
24
+
25
+    @classmethod
26
+    def from_string(cls, template: str, speed: int = DEFAULT_SPEED):
27
+        lines = template.strip().split('\n')
28
+        for line in lines:
29
+            print("line is: ", line, "+++")
30
+        grid = [[int(col) for col in line] for line in lines]
31
+        return GameOfLife(grid=grid, speed=speed)
32
+
33
+    def play(self):
34
+        while True:
35
+            self.iterate()
36
+            self.show()
37
+            time.sleep(self.speed / 1000)
38
+
39
+    def iterate(self):
40
+        new_grid = copy.deepcopy(self.grid)
41
+
42
+        for y, row in enumerate(self.grid):
43
+            for x, current_value in enumerate(row):
44
+                is_alive = current_value == 1
45
+                living_neighbors = self.get_living_neighbors(y, x)
46
+
47
+                if is_alive:
48
+                    if living_neighbors < 2:
49
+                        new_grid[y][x] = 0
50
+                    elif living_neighbors == 2 or living_neighbors == 3:
51
+                        new_grid[y][x] = 1
52
+                    else:
53
+                        new_grid[y][x] = 0
54
+                else:
55
+                    if living_neighbors == 3:
56
+                        new_grid[y][x] = 1
57
+
58
+        self.grid = new_grid
59
+
60
+    def get_living_neighbors(self, y: int, x: int):
61
+        neighbors = self.get_neighbors(y, x)
62
+        living = [n for n in neighbors if n == 1]
63
+        return len(living)
64
+
65
+    def get_neighbors(self, y: int, x: int):
66
+        neighbors = []
67
+
68
+        if y > 0:
69
+            neighbors.append(self.grid[y - 1][x])            # N
70
+            if x > 0:
71
+                neighbors.append(self.grid[y - 1][x - 1])    # NW
72
+            if x < self.width - 1:
73
+                neighbors.append(self.grid[y - 1][x + 1])    # NE
74
+
75
+        if x > 0:
76
+            neighbors.append(self.grid[y][x - 1])            # W
77
+        if x < self.width - 1:
78
+            neighbors.append(self.grid[y][x + 1])            # E
79
+
80
+        if y < self.height - 1:
81
+            neighbors.append(self.grid[y + 1][x])            # S
82
+            if x > 0:
83
+                neighbors.append(self.grid[y + 1][x - 1])    # SW
84
+            if x < self.width - 1:
85
+                neighbors.append(self.grid[y + 1][x + 1])    # SE
86
+
87
+        return neighbors
88
+
89
+    def show(self):
90
+        self.clear()
91
+
92
+        for row in self.grid:
93
+            for col in row:
94
+                if col == 0:
95
+                    # print('□', end='')
96
+                    print('·', end='')
97
+                else:
98
+                    print('■', end='')
99
+
100
+            print('\n')
101
+
102
+    def clear(self):
103
+        os.system('cls' if os.name == 'nt' else 'clear')
104
+
105
+
106
+if __name__ == "__main__":
107
+    parser = argparse.ArgumentParser(description='Conway\'s Game of Life')
108
+    parser.add_argument(
109
+        '-f', '--file', help='The path to a file containing a starting grid. The grid file should use 0 for dead cells and 1 for living cells.')
110
+    parser.add_argument(
111
+        '-s', '--speed', help='The delay between iterations in milliseconds (default: 500, minimum: 100)', default=500, type=int),
112
+    args = parser.parse_args()
113
+
114
+    if args.speed < 100:
115
+        print('Speed must be at least 100 milliseconds')
116
+        exit(1)
117
+
118
+    if args.file:
119
+        with open(args.file, 'r') as f:
120
+            contents = f.read()
121
+            GameOfLife.from_string(contents, args.speed).play()
122
+    else:
123
+        GameOfLife(args.speed).play()

+ 152
- 0
poetry.lock View File

@@ -0,0 +1,152 @@
1
+[[package]]
2
+name = "atomicwrites"
3
+version = "1.4.0"
4
+description = "Atomic file writes."
5
+category = "dev"
6
+optional = false
7
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
8
+
9
+[[package]]
10
+name = "attrs"
11
+version = "20.3.0"
12
+description = "Classes Without Boilerplate"
13
+category = "dev"
14
+optional = false
15
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
16
+
17
+[package.extras]
18
+dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"]
19
+docs = ["furo", "sphinx", "zope.interface"]
20
+tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
21
+tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"]
22
+
23
+[[package]]
24
+name = "colorama"
25
+version = "0.4.4"
26
+description = "Cross-platform colored terminal text."
27
+category = "dev"
28
+optional = false
29
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
30
+
31
+[[package]]
32
+name = "iniconfig"
33
+version = "1.1.1"
34
+description = "iniconfig: brain-dead simple config-ini parsing"
35
+category = "dev"
36
+optional = false
37
+python-versions = "*"
38
+
39
+[[package]]
40
+name = "packaging"
41
+version = "20.7"
42
+description = "Core utilities for Python packages"
43
+category = "dev"
44
+optional = false
45
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
46
+
47
+[package.dependencies]
48
+pyparsing = ">=2.0.2"
49
+
50
+[[package]]
51
+name = "pluggy"
52
+version = "0.13.1"
53
+description = "plugin and hook calling mechanisms for python"
54
+category = "dev"
55
+optional = false
56
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
57
+
58
+[package.extras]
59
+dev = ["pre-commit", "tox"]
60
+
61
+[[package]]
62
+name = "py"
63
+version = "1.9.0"
64
+description = "library with cross-python path, ini-parsing, io, code, log facilities"
65
+category = "dev"
66
+optional = false
67
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
68
+
69
+[[package]]
70
+name = "pyparsing"
71
+version = "2.4.7"
72
+description = "Python parsing module"
73
+category = "dev"
74
+optional = false
75
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
76
+
77
+[[package]]
78
+name = "pytest"
79
+version = "6.1.2"
80
+description = "pytest: simple powerful testing with Python"
81
+category = "dev"
82
+optional = false
83
+python-versions = ">=3.5"
84
+
85
+[package.dependencies]
86
+atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
87
+attrs = ">=17.4.0"
88
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
89
+iniconfig = "*"
90
+packaging = "*"
91
+pluggy = ">=0.12,<1.0"
92
+py = ">=1.8.2"
93
+toml = "*"
94
+
95
+[package.extras]
96
+checkqa_mypy = ["mypy (==0.780)"]
97
+testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
98
+
99
+[[package]]
100
+name = "toml"
101
+version = "0.10.2"
102
+description = "Python Library for Tom's Obvious, Minimal Language"
103
+category = "dev"
104
+optional = false
105
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
106
+
107
+[metadata]
108
+lock-version = "1.1"
109
+python-versions = "^3.9"
110
+content-hash = "a9c4d6be419d0ba262d5f1835c9cfe8a3441051a0fdc961b7b97c651853547ff"
111
+
112
+[metadata.files]
113
+atomicwrites = [
114
+    {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
115
+    {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
116
+]
117
+attrs = [
118
+    {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"},
119
+    {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"},
120
+]
121
+colorama = [
122
+    {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
123
+    {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
124
+]
125
+iniconfig = [
126
+    {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
127
+    {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
128
+]
129
+packaging = [
130
+    {file = "packaging-20.7-py2.py3-none-any.whl", hash = "sha256:eb41423378682dadb7166144a4926e443093863024de508ca5c9737d6bc08376"},
131
+    {file = "packaging-20.7.tar.gz", hash = "sha256:05af3bb85d320377db281cf254ab050e1a7ebcbf5410685a9a407e18a1f81236"},
132
+]
133
+pluggy = [
134
+    {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
135
+    {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
136
+]
137
+py = [
138
+    {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"},
139
+    {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"},
140
+]
141
+pyparsing = [
142
+    {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
143
+    {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
144
+]
145
+pytest = [
146
+    {file = "pytest-6.1.2-py3-none-any.whl", hash = "sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe"},
147
+    {file = "pytest-6.1.2.tar.gz", hash = "sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e"},
148
+]
149
+toml = [
150
+    {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
151
+    {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
152
+]

+ 15
- 0
pyproject.toml View File

@@ -0,0 +1,15 @@
1
+[tool.poetry]
2
+name = "conway"
3
+version = "0.1.0"
4
+description = ""
5
+authors = ["Dylan Baker <dylan@simulacrum.party>"]
6
+
7
+[tool.poetry.dependencies]
8
+python = "^3.0"
9
+
10
+[tool.poetry.dev-dependencies]
11
+pytest = "^6.1.2"
12
+
13
+[build-system]
14
+requires = ["poetry-core>=1.0.0"]
15
+build-backend = "poetry.core.masonry.api"

+ 153
- 0
test_conway.py View File

@@ -0,0 +1,153 @@
1
+from conway import GameOfLife
2
+
3
+
4
+def test_getting_neighbors():
5
+    grid = [
6
+        [0, 0, 0, 0, 0],
7
+        [0, 0, 1, 0, 0],
8
+        [0, 0, 1, 0, 0],
9
+        [0, 0, 1, 0, 0],
10
+        [0, 0, 0, 0, 0],
11
+    ]
12
+
13
+    game = GameOfLife(grid=grid)
14
+
15
+    assert game.get_neighbors(0, 1) == [0, 0, 0, 0, 1]
16
+    assert game.get_neighbors(1, 1) == [0, 0, 0, 0, 1, 0, 0, 1]
17
+    assert game.get_neighbors(2, 1) == [0, 0, 1, 0, 1, 0, 0, 1]
18
+
19
+
20
+def test_dead_cell_with_three_living_neighbors_comes_back_to_life():
21
+    grid = [
22
+        [1, 1, 1, 0, 0],
23
+        [0, 0, 0, 0, 0],
24
+        [0, 0, 0, 0, 0],
25
+        [0, 0, 0, 0, 0],
26
+        [0, 0, 0, 0, 0],
27
+    ]
28
+
29
+    game = GameOfLife(grid=grid)
30
+    game.iterate()
31
+
32
+    assert game.grid[1][1] == 1
33
+
34
+
35
+def test_dead_cells_with_three_living_neighbors_come_back_to_life():
36
+    grid = [
37
+        [0, 0, 0, 0, 0],
38
+        [0, 0, 0, 0, 0],
39
+        [0, 1, 1, 1, 0],
40
+        [0, 0, 0, 0, 0],
41
+        [0, 0, 0, 0, 0],
42
+    ]
43
+
44
+    game = GameOfLife(grid=grid)
45
+    game.iterate()
46
+
47
+    assert game.grid[1][2] == 1
48
+    assert game.grid[3][2] == 1
49
+
50
+
51
+def test_living_cell_with_under_two_living_neighbors_dies():
52
+    grid = [
53
+        [0, 0, 0, 0, 0],
54
+        [0, 0, 0, 0, 0],
55
+        [0, 0, 1, 0, 0],
56
+        [0, 0, 0, 0, 0],
57
+        [0, 0, 0, 0, 0],
58
+    ]
59
+
60
+    game = GameOfLife(grid=grid)
61
+    game.iterate()
62
+
63
+    assert game.grid[1][2] == 0
64
+    assert game.grid[2][2] == 0
65
+
66
+
67
+def test_living_cell_with_two_or_three_neighbors_survives():
68
+    grid = [
69
+        [0, 0, 0, 0, 0],
70
+        [0, 0, 1, 0, 0],
71
+        [0, 0, 1, 0, 0],
72
+        [0, 0, 1, 0, 0],
73
+        [0, 0, 0, 0, 0],
74
+    ]
75
+
76
+    game = GameOfLife(grid=grid)
77
+    game.iterate()
78
+
79
+    assert game.grid[2][2] == 1
80
+
81
+
82
+def test_blinker():
83
+    grid = [
84
+        [0, 0, 0, 0, 0],
85
+        [0, 0, 1, 0, 0],
86
+        [0, 0, 1, 0, 0],
87
+        [0, 0, 1, 0, 0],
88
+        [0, 0, 0, 0, 0],
89
+    ]
90
+
91
+    game = GameOfLife(grid=grid)
92
+
93
+    game.iterate()
94
+
95
+    assert game.grid == [
96
+        [0, 0, 0, 0, 0],
97
+        [0, 0, 0, 0, 0],
98
+        [0, 1, 1, 1, 0],
99
+        [0, 0, 0, 0, 0],
100
+        [0, 0, 0, 0, 0],
101
+    ]
102
+
103
+    game.iterate()
104
+
105
+    assert game.grid == grid
106
+
107
+
108
+def test_beacon():
109
+    grid = [
110
+        [0, 0, 0, 0, 0, 0],
111
+        [0, 1, 1, 0, 0, 0],
112
+        [0, 1, 0, 0, 0, 0],
113
+        [0, 0, 0, 0, 1, 0],
114
+        [0, 0, 0, 1, 1, 0],
115
+        [0, 0, 0, 0, 0, 0],
116
+    ]
117
+
118
+    game = GameOfLife(grid=grid)
119
+
120
+    game.iterate()
121
+
122
+    assert game.grid == [
123
+        [0, 0, 0, 0, 0, 0],
124
+        [0, 1, 1, 0, 0, 0],
125
+        [0, 1, 1, 0, 0, 0],
126
+        [0, 0, 0, 1, 1, 0],
127
+        [0, 0, 0, 1, 1, 0],
128
+        [0, 0, 0, 0, 0, 0],
129
+    ]
130
+
131
+    game.iterate()
132
+
133
+    assert game.grid == grid
134
+
135
+
136
+def test_creating_grid_from_string():
137
+    s = '''
138
+00000
139
+00000
140
+01110
141
+00000
142
+00000
143
+    '''
144
+
145
+    game = GameOfLife.from_string(s)
146
+
147
+    assert game.grid == [
148
+        [0, 0, 0, 0, 0],
149
+        [0, 0, 0, 0, 0],
150
+        [0, 1, 1, 1, 0],
151
+        [0, 0, 0, 0, 0],
152
+        [0, 0, 0, 0, 0],
153
+    ]

Loading…
Cancel
Save