{"id":17973001,"url":"https://github.com/jnyjny/gameoflife","last_synced_at":"2025-03-25T12:33:16.417Z","repository":{"id":57432988,"uuid":"44756927","full_name":"JnyJny/GameOfLife","owner":"JnyJny","description":"Conway's Game of Life","archived":false,"fork":false,"pushed_at":"2020-02-10T20:20:37.000Z","size":25128,"stargazers_count":19,"open_issues_count":0,"forks_count":3,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-03-20T08:44:55.369Z","etag":null,"topics":["asciimatics","conway-game","conways-game-of-life","curses","game-of-life","gameoflife","life","pygame","python","simulation"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/JnyJny.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-10-22T16:04:12.000Z","updated_at":"2025-01-21T10:40:24.000Z","dependencies_parsed_at":"2022-08-27T20:51:59.964Z","dependency_job_id":null,"html_url":"https://github.com/JnyJny/GameOfLife","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JnyJny%2FGameOfLife","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JnyJny%2FGameOfLife/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JnyJny%2FGameOfLife/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JnyJny%2FGameOfLife/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/JnyJny","download_url":"https://codeload.github.com/JnyJny/GameOfLife/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245463042,"owners_count":20619601,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["asciimatics","conway-game","conways-game-of-life","curses","game-of-life","gameoflife","life","pygame","python","simulation"],"created_at":"2024-10-29T16:26:51.003Z","updated_at":"2025-03-25T12:33:13.182Z","avatar_url":"https://github.com/JnyJny.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# GameOfLife\nConway's Game of Life - Cellular Automata in Python\n\nThis is a python3 package that provides two classes\nthat together implement Conway's Game of Life. \n\n```\n\u003e\u003e\u003e from GameOfLife import *\n\u003e\u003e\u003e w = World()\n\u003e\u003e\u003e w.addPattern('glider')\n\u003e\u003e\u003e while True:\n\u003e\u003e\u003e     w.step()\n\u003e\u003e\u003e     print(w)\n```\n\n## Install\n\nYou can use pip to install ```GameOfLife```:\n\n```\n$ sudo pip3 install GameOfLife\n```\n\nYou can clone this repository\n\n```\n$ git clone https://github.com/JnyJny/GameOfLife.git\n$ cd GameOfLife\n$ python3 setup.py install\n```\n\n## Examples\n\nI've provided two examples in the contrib directory of the\nrepo: **CGameOfLife** and **PGameOfLife**. The **C** stands for\ncurses and will show a simulation in a terminal window using\nthe time-honored curses library. The **P** in PGameOfLife stands\nfor [**PyGame**][4] and draws the simulation in a pygame window.\n\n### CGameOfLife Demo\n\n![CGameOfLife Demo][2]\n\n### PGameOfLife Demo\n\n![PGameOfLife Demo][3]\n\n### [CP]GameOfLife Usage\n```\n$ [CP]GameOfLife.py [pattern_name[,X,Y]] ...\n...\n$ [CP]GameOfLife.py foo\nunknown pattern: 'foo'\nknown patterns:\n\tblock\n\tlws\n\ttoad\n\tpulsar\n\tloaf\n\tglider\n\tblinker\n\tbeehive\n\tbeacon\n\tboat\n$ [CP]GameOfLife.py glider,10,10 pulsar,0,0 lws,0,20\n...\t\n```\n\n## Design Notes - The Fun Stuff\n\nThere are lots of ways of representing a Game of Life board but I\ndecided to write it as a two dimensional grid of cell objects. The\ngrid would organize the cells and the cells would implement the\nrules that would determine their state: alive or dead. The grid\nis called the ```World``` and the cells are called... ```Cell```.\n\nAn obvious drawback of this design is that it is memory inefficient:\neach display element is represented by a ```Cell``` object. Assuming a two\ncolor display, you would only need one bit per display element to\nmodel the state of each state. A ```Cell``` object is much larger than\nthat. That said, my goal was to practice writing good objects \nand then experiencing how this solution would evolve.\n\nThe first technical problem, of course, is that python doesn't have a\nnative two dimensional grid object. I picked a list object as my\nfoundational grid object and overrode the ```__getitem__``` method to\nimplement accessing elements using x and y coordinates.\n\nThis also made it easier to implement an \"infinite\" grid by wrapping\nthe edges when accessed by (x,y) and still allowed iterating through\nall the cells serially.\n\nNow that we can access ```Cell```s, it's time to decide what the\n```Cell``` should do. Each cell tracks it's current state: alive or\ndead and has to know what rules it should be following. In order to to\ncompute it's next state, it requires the number of living cells in\nit's immediate environ.  Consider a cell embedded in a rectangular\nmatrix; this cell has eight neigbors in the 3x3 sub-matrix where the\ntarget cell has coordinates (1,1). \n\nTechnically, cells don't really need to know where they are in the\nworld, they only care about the current state of their\nneighbors. However the cells cannot directly access neighboring cells,\nas that would violate division of labor between the cell and grid data\nstructures. I also didn't want the ```Cell``` to have some weird back\nreference to the ```World``` object. \n\nBecause the cells know their own address, they can compute the\naddresses of their neighbors. This simplifies the ```World.step```\nmethod since it doesn't need to compute each cell's neighbors as it\niterates through all the cells. One step further would be to keep\na list of the actual neighbor cells. The ```World.step``` method\nwould become even more simple since it would not have to provide\nany information to a cell other than it's time to compute the next\nstate.\n\nAt the end of the day, the step function was a O(2*n) method since\neach cell was visited in two batches: the first to record the number\nof alive neighbors for each cell and the second to change state based\non the number of alive neighbors. Most of the time this is implemented\nas a frame buffer type data structure, with one frame indicating the\nn-th step and the other frame the n+1-th step. It's a classic space\nfor time trade off.\n\n### Patterns\n\nI have provided a few simple patterns to prove that the simulation is\nworking. The ```Patterns``` dictionary is keyed with each pattern's\nname and then a string representing cell states: spaces are not-alive\ncells, non-spaces are alive cells. A newline in the string indicates a\nnew row.\n\n### Optimizations\n\nAfter I wrote it and started thinking about optimizations, I began\nto think about replacing the ```Cell``` model with a numpy array to take\nadvantage of presumably optimized array accessors.  Or even just an\narray of characters and letting the character value encode the cell\nstate in a more compact (and obtuse) manner. Those changes are pretty\ndisruptive and I decided to postpone them.\n\n#### Neighbors\n\nAs far as simple but useful optimizations, caching each cell's\nneighbor coordinates traded space for time when calculating the status\nof each cell's neighbors in the ```step``` method. This simplifies the\nmethod since it isn't necessary to pretend the list is a two dimension\nobject; just iterate through all the cells and ask each cell who\nit's neighbors are. A better optimization might be to calculate the 1D\nindex rather than the 2D, but I think this brings too much knowledge\nof the ```World```'s implementation into the cell.\n\n#### Hash Cache FTW\n\nIn order for a python object to participate in a set container, it\nneeds to implement a ```__hash__``` method which should return a\nunique hash value for the object. One lesson I learned: cache the hash\nvalue of an object. It is much more efficient that computing the hash\nvalue everytime the method is called. I suppose this is not a valid\noptimization for object's whose hash value can mutate over time,\nhowever ```Cell```s do not migrate around the ```World``` and the\nuniqueness of the hash is driven by the location of the cell.\n\n#### Dead Cells Don't Matter\n\nThe best optimization came when I realized that the ```World``` is\nonly affected by living cells and I could achieve a more efficient\n```step``` method if I only visited living cells and their immediate\nsurroundings. It is not a perfect solution as a ```World``` could have\nnearly all cells living and thus the number of alive cells would tend\nto the number of total cells. However, that case is generally rare and\nthe number of live cells is usually much less than the total number of\ncells in the world. [Cite needed :]\n\n#### OptimizedWorld\n\nI added a new class, ```OptimizedWorld```, and overrode only a couple\nof methods on ```World```: ```reset```, ```addPattern```, and\n```step```. I then imbued the class with a new property, a set named\n```alive``` which is initialized with all the live cells in the\n```addPattern``` method.\n\nThe magic happens in the ```step``` method:\n\n1. Build a set of all the neighbors of all the live cells in the world.\n2. Update each cell with the number of live neighbors it has.\n2. Apply the live neighbor count to the cell to drive a state change.\n3. Finally, pull out any dead cells from the live set.\n4. Rinse and repeat.\n\nThis optimization resulted in some very impressive gains in speed;\nfrom roughly 10 generations a second to around 100 generations a second\nmeasured in the curses CGameOfLife implementation (on a Mid 2014 Mac Book\nPro with a 2.8Ghz i7, 16G of memory and NVIDIA graphics).\n\nThe algorithm is O((alive+neighbors) x 2) where normally alive \u003c\u003c total\nnumber of cells and neighbors is bounded by [0,alive x 8]. I think, my\nalgorithm analysis is admittedly rusty. \n\n[1]: https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life\n[2]: https://github.com/JnyJny/GameOfLife/blob/master/Screenshots/CGameOfLife-Demo.gif\n[3]: https://github.com/JnyJny/GameOfLife/blob/master/Screenshots/PGameOfLife-Demo.gif\n[4]: http://pygame.org\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjnyjny%2Fgameoflife","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjnyjny%2Fgameoflife","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjnyjny%2Fgameoflife/lists"}