Skip to content

Instantly share code, notes, and snippets.

@taylor8294
Last active April 22, 2018 17:25
Show Gist options
  • Select an option

  • Save taylor8294/e157dd78dcd847c32f05cfd860cabea8 to your computer and use it in GitHub Desktop.

Select an option

Save taylor8294/e157dd78dcd847c32f05cfd860cabea8 to your computer and use it in GitHub Desktop.
Conway's Game Of Life in 'Pythonista for iOS'
/*
* 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(",")
#!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)
# 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