Last active
April 22, 2018 17:25
-
-
Save taylor8294/e157dd78dcd847c32f05cfd860cabea8 to your computer and use it in GitHub Desktop.
Conway's Game Of Life in 'Pythonista for iOS'
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * A simple bit of javascript to calculate the next state of a cell in Conway's | |
| * Game of Life for all possible 9-cell neighbourhoods, and outputs these as an | |
| * array where each possible neighbourhood is represented as a number between | |
| * 0-511, ie. the indices of the returned array. This is how I got the | |
| * `nghbhd_to_state` variable in GameOfLife.py. | |
| * I'm hoping using this pre-calculated array within Pythonista will help speed | |
| * up the implementation. | |
| */ | |
| function dec2bin(dec){ | |
| return (dec >>> 0).toString(2); | |
| } | |
| function add(a, b) { | |
| return a + b; | |
| } | |
| var ngbhds = []; | |
| for(i=0;i<512;i++) | |
| ngbhds.push(dec2bin(i).padStart(9,'0').split('').map(Number)); | |
| results = [] | |
| ngbhds.forEach(function(ngbhd,i,a){ | |
| current_state = ngbhd[4]; | |
| sum = ngbhd.reduce(add)-current_state; | |
| if(current_state) | |
| result = sum == 2 || sum == 3 ? 1 : 0 | |
| else | |
| result = sum == 3 ? 1 : 0 | |
| results[parseInt(ngbhd.join(''), 2)] = result | |
| }); | |
| results.join(",") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!python3 | |
| # coding: utf-8 | |
| ''' | |
| A "Conway's Game Of Life" implementation for use with (Pythonista for iOS)[http://omz-software.com/pythonista/] (v3). | |
| Features include: | |
| * Start menu with ability to choose from a selection of intial states | |
| * Pause button | |
| * Tap the screen to bring cells to life | |
| * Tap with two fingers to pause the game (without the menu) to allow you to draw cells. Tap again with two fingers to play. | |
| Roadmap / Needed / To do: | |
| * Major efficiency optimisations | |
| Contributors: Alex Taylor (taylor894) | |
| Link: http://taylrr.co.uk/ | |
| Tags: conway, game, life, gameoflife, python, pythonista, ios | |
| Tested up to: Pythonista v3.0 | |
| Version: 1.0 | |
| License: GPLv3 | |
| License URI: https://www.gnu.org/licenses/gpl-3.0.en.html | |
| License Info: Allows modification and distribution. Limits liability and warranty. | |
| On the condition of license and copyright notice, stating any changes, | |
| disclose source (attribution), use same license. | |
| ''' | |
| from scene import * | |
| from game_menu import MenuScene | |
| from gameoflife_states import * | |
| import sys | |
| A = Action | |
| # Globals | |
| cell_w = 16 #iPhone | |
| cell_h = 16 | |
| cell_new_col = '#ff9' | |
| cell_on_col = '#eee' | |
| cell_off_col = '#111' | |
| cell_new_stroke_col = '#222' | |
| cell_on_stroke_col = '#222' | |
| cell_off_stroke_col = '#222' | |
| cell_stroke_w = 2 | |
| cell_shadow_col = None | |
| cell_shadow_x = 4 | |
| cell_shadow_y = 4 | |
| cell_shadow_r = 5 | |
| # Helper functions | |
| nghbhd_to_state = (0,0,0,0,0,0,0,1,0,0,0,1,0,1,1,0,0,0,0,1,0,1,1,1,0,1,1,1,1,1,1,0,0,0,0,1,0,1,1,0,0,1,1,0,1,0,0,0,0,1,1,1,1,1,1,0,1,1,1,0,1,0,0,0,0,0,0,1,0,1,1,0,0,1,1,0,1,0,0,0,0,1,1,1,1,1,1,0,1,1,1,0,1,0,0,0,0,1,1,0,1,0,0,0,1,0,0,0,0,0,0,0,1,1,1,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,1,1,0,0,1,1,0,1,0,0,0,0,1,1,1,1,1,1,0,1,1,1,0,1,0,0,0,0,1,1,0,1,0,0,0,1,0,0,0,0,0,0,0,1,1,1,0,1,0,0,0,1,0,0,0,0,0,0,0,0,1,1,0,1,0,0,0,1,0,0,0,0,0,0,0,1,1,1,0,1,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,1,0,0,1,1,0,1,0,0,0,0,1,1,1,1,1,1,0,1,1,1,0,1,0,0,0,0,1,1,0,1,0,0,0,1,0,0,0,0,0,0,0,1,1,1,0,1,0,0,0,1,0,0,0,0,0,0,0,0,1,1,0,1,0,0,0,1,0,0,0,0,0,0,0,1,1,1,0,1,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,1,0,0,0,1,0,0,0,0,0,0,0,1,1,1,0,1,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0) | |
| # SpriteNode subclass representing a cell: | |
| class Cell (object): | |
| def __init__(self, x, y, w, h, alive=False, *args, **kwargs): | |
| rect = ui.Path.rect(x, y, w, h) | |
| rect.line_width = cell_stroke_w | |
| cell_col = cell_off_col if alive == False else cell_on_col | |
| cell_stroke_col = cell_off_stroke_col if alive == False else cell_on_stroke_col | |
| self.shape = ShapeNode(rect, cell_col, cell_stroke_col, (cell_shadow_col, cell_shadow_x, cell_shadow_y, cell_shadow_r), position=(x, y), anchor_point=(0,0)) | |
| self.__state = alive | |
| self.__is_new = alive | |
| def get_state(self): | |
| return '1' if self.__state else '0' | |
| def set_state(self, alive=None): | |
| if alive is None: | |
| alive = not self.__state | |
| elif alive == self.__state: | |
| if alive: | |
| self.__is_new = False | |
| self.shape.fill_color = cell_on_col | |
| self.shape.stroke_color = cell_on_stroke_col | |
| return | |
| self.__is_new = alive | |
| self.shape.fill_color = cell_new_col if alive else cell_off_col | |
| self.shape.stroke_color = cell_new_stroke_col if alive else cell_off_stroke_col | |
| self.__state = bool(alive) | |
| def is_alive(self): | |
| return self.__state | |
| def is_dead(self): | |
| return not self.__state | |
| def is_new(self): | |
| return self.__is_new | |
| class Grid(object): | |
| def __init__(self, screen_size): | |
| self.cols = int(screen_size.w/cell_w) | |
| self.rows = int(screen_size.h/cell_h) | |
| #cell_w, cell_h = (screen_size.w/cols, screen_size.h/rows) | |
| self.padding_left = (screen_size.w-cell_w*self.cols)/2.0 | |
| self.cells = { | |
| (col, row) : Cell(self.padding_left+col*cell_w, row*cell_h, cell_w, cell_h) | |
| for col in list(range(self.cols)) | |
| for row in list(range(self.rows)) | |
| } | |
| self.next_gen_changes = {} | |
| self.alive_nghbhd = set() | |
| print('Created', self.cols, 'by', self.rows,'board') | |
| def turn_on(self, idx): | |
| self.cells[idx].set_state(True) | |
| def turn_off(self, idx): | |
| self.cells[idx].set_state(False) | |
| def set_state(self, idx, alive=None): | |
| self.cells[idx].set_state(alive) | |
| def set_states(self, idxs, states): | |
| for idx, state in zip(idxs,states): | |
| self.cells[idx].set_state(state) | |
| def get_alive(self, loopAll=False): | |
| alive = set() | |
| if loopAll: | |
| for idx in self.cells.keys(): | |
| if self.cells[idx].is_alive(): | |
| alive.add(idx) | |
| return alive | |
| if len(self.alive_nghbhd) > 0: | |
| for idx in self.alive_nghbhd: | |
| if self.cells[idx].is_alive(): | |
| alive.add(idx) | |
| return alive | |
| def get_nghbhd_idxs(self, idx): | |
| x, y = idx | |
| nghb_idxs = [(-1,-1),(0,-1),(1,-1),(-1,0),(0,0),(1,0),(-1,1),(0,1),(1,1)] | |
| i = 0 | |
| for dx, dy in nghb_idxs: | |
| nx = x+dx | |
| ny = y+dy | |
| if nx < 0: nx = self.cols-1 | |
| if nx >= self.cols: nx = 0 | |
| if ny < 0: ny = self.rows-1 | |
| if ny >= self.rows: ny = 0 | |
| nghb_idxs[i] = (nx,ny) | |
| i += 1 | |
| return nghb_idxs | |
| def get_nghbhd_num(self, idx): | |
| nghb_idxs = self.get_nghbhd_idxs(idx) | |
| bin = '' | |
| for nx, ny in nghb_idxs: | |
| bin += self.cells[(nx,ny)].get_state() | |
| return int(bin, 2) | |
| def get_all_nghbhds_set(self, idxs): | |
| all_nghbhds_set = set() | |
| for idx in idxs: | |
| nghb_idxs = self.get_nghbhd_idxs(idx) | |
| all_nghbhds_set |= set(nghb_idxs) | |
| return all_nghbhds_set | |
| # TODO: Make more efficient, use 2D funcs | |
| def screen_to_grid(self, coord): | |
| p = Point(coord[0], coord[1]) | |
| for idx, cell in self.cells.items(): | |
| if p in cell.shape.frame: | |
| return idx | |
| # TODO: Make more efficieint | |
| # qtree? | |
| def update(self, loopAll=False): | |
| self.next_gen_changes = {} | |
| self.alive_nghbhd = self.get_all_nghbhds_set(self.get_alive(loopAll)) | |
| #counter = 0 | |
| for idx in self.alive_nghbhd: | |
| #counter += 1 | |
| toState = nghbhd_to_state[self.get_nghbhd_num(idx)]==1 | |
| if (toState is not self.cells[idx].is_alive()) or self.cells[idx].is_new(): | |
| self.next_gen_changes[idx] = toState | |
| #print('Calc\'d next state', counter, 'times') | |
| #counter = 0 | |
| for idx in self.next_gen_changes.keys(): | |
| self.cells[idx].set_state(self.next_gen_changes[idx]) | |
| #counter += 1 | |
| #print('Made', counter, 'changes') | |
| def reset(self, deep=False): | |
| if deep: | |
| for idx in self.cells.keys(): | |
| self.cells[idx].shape.remove_from_parent() | |
| self.cells = [] | |
| self.cells = { | |
| (col, row) : Cell(self.padding_left+col*cell_w, row*cell_h, cell_w, cell_h) | |
| for col in list(range(self.cols)) | |
| for row in list(range(self.rows)) | |
| } | |
| else: | |
| for idx in self.cells.keys(): | |
| self.cells[idx].set_state(False) | |
| # The actual game logic: | |
| class Game (Scene): | |
| def setup(self): | |
| self.gen = 0 | |
| self.paused = True | |
| self.after_touch = False | |
| self.sleep_delay = 0 | |
| self.initial_state = 0 | |
| if self.size.w > 760: #iPad | |
| cell_w, cell_h = 32, 32 | |
| self.grid = Grid(self.size) | |
| self.load_initial_state() | |
| for idx in self.grid.cells.keys(): | |
| self.add_child(self.grid.cells[idx].shape) | |
| self.pause_button = SpriteNode('iow:pause_32', position=(32, self.size.h-32), parent=self) | |
| self.pause_button.z_position = 2 | |
| self.gen_label = LabelNode('0', font=('Avenir Next', 40), position=(self.size.w/2, self.size.h-50), parent=self) | |
| self.gen_label.z_position = 2 | |
| self.background_color = '#000' | |
| if self.sleep_delay > 0: | |
| self.run_action(A.repeat( | |
| A.sequence( | |
| A.wait(self.sleep_delay), | |
| A.call(self.do_update) | |
| ), 0 | |
| )) | |
| else: | |
| self.run_action(A.repeat( | |
| A.sequence( | |
| A.call(self.do_update) | |
| ), 0 | |
| )) | |
| self.show_start_menu() | |
| def do_update(self): | |
| if self.paused == False: | |
| self.grid.update(self.after_touch) | |
| self.gen += 1 | |
| self.gen_label.text = str(self.gen) | |
| if len(self.grid.next_gen_changes)==0: | |
| self.paused = True | |
| # TODO | |
| def fetch_initial_state(self): | |
| try: | |
| with open('.gameoflife_states', 'r') as f: | |
| self.initial_state = int(f.read()) | |
| except: | |
| self.initial_state = None | |
| # TODO | |
| def save_state(self): | |
| with open('.gameoflife_states', 'w') as f: | |
| f.write(str(self.highscore)) | |
| def new_game(self): | |
| self.gen = 0 | |
| self.load_initial_state() | |
| def load_initial_state(self): | |
| lines = initial_states[self.initial_state].splitlines() | |
| self.grid.reset() | |
| left_space = max(int((self.grid.cols-len(lines[0]))/2),0) | |
| bottom_space = max(int((self.grid.rows-len(lines))/2),0) | |
| for y, line in enumerate(reversed(lines)): | |
| if y >= self.grid.rows: | |
| break | |
| for x, char in enumerate(line): | |
| if x >= self.grid.cols: | |
| break | |
| if char == '1': | |
| self.grid.cells[(left_space+x,bottom_space+y)].set_state(True) | |
| self.grid.alive_nghbhd = self.grid.get_all_nghbhds_set(self.grid.get_alive(True)) | |
| def touch_began(self, touch): | |
| x, y = touch.location | |
| if x < 48 and y > self.size.h - 48: | |
| self.show_pause_menu() | |
| else: | |
| if len(self.touches)>1: | |
| self.paused = not self.paused | |
| if not self.paused: | |
| self.after_touch = True | |
| #self.paused = True if not self.paused else False | |
| idx = self.grid.screen_to_grid(touch.location) | |
| self.grid.set_state(idx) | |
| self.grid.alive_nghbhd |= set(self.grid.get_nghbhd_idxs(idx)) | |
| def touch_moved(self, touch): | |
| idx = self.grid.screen_to_grid(touch.location) | |
| self.grid.set_state(idx) | |
| self.grid.alive_nghbhd |= set(self.grid.get_nghbhd_idxs(idx)) | |
| def show_start_menu(self): | |
| self.paused = True | |
| self.menu = MenuScene('Game Of Life', 'Gen: %i' % self.gen, ['Play', 'Initial: ' + initial_states_names[self.initial_state]]) | |
| self.present_modal_scene(self.menu) | |
| def show_pause_menu(self): | |
| self.paused = True | |
| self.menu = MenuScene('Paused', 'Gen: %i' % self.gen, ['Continue', 'New Game']) | |
| self.present_modal_scene(self.menu) | |
| def menu_button_selected(self, title): | |
| if title.startswith('Initial:'): | |
| self.initial_state = (self.initial_state + 1) % len(initial_states_names) | |
| self.load_initial_state() | |
| return 'Initial: ' + initial_states_names[self.initial_state] | |
| elif title in ('Continue', 'New Game', 'Play'): | |
| self.dismiss_modal_scene() | |
| self.menu = None | |
| self.paused = False | |
| if title in ('New Game', 'Play'): | |
| self.new_game() | |
| # Run the game: | |
| if __name__ == '__main__': | |
| run(Game(), PORTRAIT, show_fps=True) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Stores various initial states to be imported into the Pythonista 'Game Of Life' game | |
| initial_states = [] | |
| initial_states_names = [] | |
| initial_states.append('''010 | |
| 001 | |
| 111''') | |
| initial_states_names.append('glider') | |
| initial_states.append('1111111111') | |
| initial_states_names.append('tenRow') | |
| initial_states.append('''01111 | |
| 10001 | |
| 00001 | |
| 10010''') | |
| initial_states_names.append('spaceship') | |
| initial_states.append('''0110110 | |
| 0110110 | |
| 0010100 | |
| 1010101 | |
| 1010101 | |
| 1100011''') | |
| initial_states_names.append('tumbler') | |
| initial_states.append('''000000000000000000000000100000000000 | |
| 000000000000000000000010100000000000 | |
| 000000000000110000001100000000000011 | |
| 000000000001000100001100000000000011 | |
| 110000000010000010001100000000000000 | |
| 110000000010001011000010100000000000 | |
| 000000000010000010000000100000000000 | |
| 000000000001000100000000000000000000 | |
| 000000000000110000000000000000000000''') | |
| initial_states_names.append('gliderGun') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment