wasp: apps: Conway's Game of Life

This commit is contained in:
Daniel Thompson 2020-04-17 17:30:31 +01:00
parent bfd914a5d1
commit 880764da7d
2 changed files with 246 additions and 0 deletions

BIN
res/gameoflife.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

246
wasp/apps/gameoflife.py Normal file
View File

@ -0,0 +1,246 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
# Copyright (C) 2020 Daniel Thompson
"""Conway's Game of Life.
On 11 April 2020 John H. Conway who, among many, many other
achievements, devised the rule set for his Game of Life, died of
complications from a COVID-19 infection.
The Game of Life is the first "toy" program I ever recall seeing on a
computer (running in a mid 1980s Apple Macintosh). It sparked something
even if "toy" is perhaps an underwhelming description of the Game of Life.
Either way it occupies a special place in my childhood. For that, this
application is dedicated to Professor Conway.
"""
import array
import machine
import micropython
import wasp
@micropython.viper
def xorshift12(v: int) -> int:
"""12-bit xorshift pseudo random number generator.
With only 12-bits of state this PRNG is another toy! It appears
here because it allows us to visit every possible 12-bit value
(except zero) whilst taking an interesting route. This allows us to
make the redraw (which is too slow to fully conceal) visually
engaging.
"""
v ^= v << 1
v ^= (v >> 3) & 0x1ff
v ^= (v << 7)
return v & 0xfff
@micropython.viper
def get_color(v: int) -> int:
r = v >> 10
g = (v >> 8) & 7
b = (v >> 5) & 3
return (r << 13) | (g << 7) | (b << 1) | 0x9c73
@micropython.viper
def get_cell(board, stride: int, x: int, y: int) -> bool:
b = ptr32(board)
xw = x >> 5
xb = x & 0x1f
yw = y * (stride >> 5)
return bool(b[yw + xw] & (1 << xb))
@micropython.viper
def set_cell(board, stride: int, x: int, y: int, v: bool):
b = ptr32(board)
xw = x >> 5
xb = x & 0x1f
yw = y * (stride >> 5)
m = 1 << xb
c = b[yw + xw]
# viper doesn't implement bitwise not so we are having
# to clear bits using xor...
if v:
b[yw + xw] = c | m
elif c & m:
b[yw + xw] = c ^ m
@micropython.viper
def game_of_life(b, xmax: int, ymax: int, nb):
"""Run a single generation of Conway's Game of Life
1. Death by isolation: a cell dies if has fewer than two live neighbours.
2. Death by overcrowding: a cell dies if it has more than three live
neighbours.
3. Survival: a living cell continues to survive if it has two or three
neighbours.
4. Reproduction: a dead cell comes alive if it has exactly three
neighbours.
In the code below we have simplified the above rules to "a cell is
alive it has three live neighbours or if it was previously alive
and has two neighbours, otherwise it is dead.".
"""
board = ptr32(b)
next_board = ptr32(nb)
for y in range(1, ymax-1):
tm = int(get_cell(board, xmax, 0, y-1))
tr = int(get_cell(board, xmax, 1, y-1))
cm = int(get_cell(board, xmax, 0, y))
cr = int(get_cell(board, xmax, 1, y))
bm = int(get_cell(board, xmax, 0, y+1))
br = int(get_cell(board, xmax, 1, y+1))
for x in range(1, xmax-1):
tl = tm
tm = tr
tr = int(get_cell(board, xmax, x+1, y-1))
cl = cm
cm = cr
cr = int(get_cell(board, xmax, x+1, y))
bl = bm
bm = br
br = int(get_cell(board, xmax, x+1, y+1))
c = tl + tm + tr + cl + cr + bl + bm + br
set_cell(next_board, xmax, x, y, c == 3 or (cm and c == 2))
# 2-bit RLE, generated from res/gameoflife.png, 404 bytes
# The icon is a carefully selected generation of an "acorn", I wanted
# to avoid using a glider, they are overused to the point of cliche!
icon = (
b'\x02'
b'`@'
b'?\xff\xff\xee@\xd7B\x02B\x02B?\x16L?\x15'
b'L?\x16B\x02B\x02B?\x1bB?\x1eD?\x1d'
b'D?\x1eB?\x17\x80\xbe\x82\x02\x82\x06\x82\x02\x82?'
b'\x0e\x88\x04\x88?\r\x88\x04\x88?\x0e\x82\x02\x82\x06B'
b'\x02\x82?\x03\xc0\x97\xc2\x02\xc2\x02\xc2\x02\xc2\x02\xc2\x02'
b'\xc2\x02\xc2\x02\xc2\x02\xc2\x02\xc2\x02\xc25\xec4\xec5'
b'\xc2\x02\xc2\x02\xc2\x02\xc2\x02\xc2\x02\xc2\x02\xc2\x02\xc2\x02'
b'\xc2\x02\xc2\x02\xc2*B\x02B\x12\xc2\x06B\x06\xc2\x12'
b'B\x02B\x1dH\x10\xc4\x04D\x04\xc4\x10H\x1cH\x10'
b'\xc4\x04D\x04\xc4\x10H\x1dB\x02B\x12\xc2\x06B\x06'
b'\xc2\x12B\x02B\x1eB\x16\xc2\x0e\xc2\x16B\x1dD\x14'
b'\xc4\x0c\xc4\x14D\x1cD\x14\xc4\x0c\xc4\x14D\x1dB\x16'
b'\xc2\x0e\xc2\x16B\x1eB>B\x1dD<D\x1cD<'
b'D\x1dB>B"B\x02B\x06B\x02B\x02B\x0e'
b'B\x02B\x02B\x06B\x02B%H\x04L\x0cL\x04'
b'H$H\x04L\x0cL\x04H%B\x02B\x06B\x02'
b'B\x02B\x0eB\x02B\x02B\x06B\x02B2B\n'
b'B\x06B\nB=D\x08D\x04D\x08D<D\x08'
b'D\x04D\x08D=B\nB\x06B\nB>B\x02'
b'B\x06\x82\x06\x82\x06B\x02B=H\x04\x84\x04\x84\x04'
b'H<H\x04\x84\x04\x84\x04H=B\x02B\x06\x82\x06'
b'\x82\x06B\x02B>\x82\x02\x82\x16\x82\x02\x82=\x88\x14'
b'\x88<\x88\x14\x88=\x82\x02\x82\x16\x82\x02\x82>\x82\x02'
b'\x82\x02\x82\x0e\x82\x02\x82\x02\x82=\x8c\x0c\x8c<\x8c\x0c'
b'\x8c=\x82\x02\x82\x02\x82\x0e\x82\x02\x82\x02\x82?\xff\xff'
b'\xe2'
)
class GameOfLifeApp():
"""Application implementing Conway's Game of Life.
"""
NAME = 'Life'
ICON = icon
def __init__(self):
"""Initialize the application."""
self._board = array.array('I', [0] * (64*64//32))
self._next_board = array.array('I', self._board)
self._color = 1
self.touch(None)
def foreground(self):
"""Activate the application."""
self._draw()
wasp.system.request_event(wasp.EventMask.TOUCH)
wasp.system.request_tick(1000)
def tick(self, ticks):
"""Notify the application that its periodic tick is due."""
wasp.system.keep_awake()
#t = machine.Timer(id=1, period=8000000)
#t.start()
game_of_life(self._board, 64, 64, self._next_board)
#t1 = t.time()
self._update()
#t2 = t.time()
#t.stop()
#del t
#wasp.watch.drawable.string('{:4.2f}s {:4.2f}s'.format(t1 / 1000000,
# t2 / 1000000), 6, 210)
def touch(self, event):
"""Notify the application of a touchscreen touch event."""
board = self._next_board
for i in range(len(board)):
board[i] = 0
board[62] = 32 << 16
board[64] = 8 << 16
board[66] = 103 << 16
if None != event:
self._update()
def _draw(self):
"""Draw the display from scratch."""
wasp.watch.drawable.fill()
board = self._board
for i in range(len(board)):
board[i] = 0
self._update()
def _update(self):
"""Update the dynamic parts of the application display."""
b = self._board
nb = self._next_board
self._board = nb
self._next_board = b
display = wasp.watch.display
lb = display.linebuffer
alive = memoryview(lb)[0:2*16]
self._color = xorshift12(self._color)
rgbhi = get_color(self._color)
rgblo = rgbhi & 0xff
rgbhi >>= 8
for i in range(0, len(alive), 2):
alive[i] = rgbhi
alive[i+1] = rgblo
for i in (0, 3, 12, 15):
alive[i*2] = 0
alive[i*2+1] = 0
dead = memoryview(lb)[2*16:4*16]
for i in range(len(dead)):
dead[i] = 0
def draw_cell(cell, display, px):
x = ((cell & 0x3f) - 2) * 4
y = ((cell >> 6) -2) * 4
if x < 0 or x >= 240 or y < 0 or y >= 240:
return
display.set_window(x, y, 4, 4)
display.write_data(px)
draw_cell(1, display, alive if b[1//32] & (1 << (1 & 0x1f)) else dead)
v = xorshift12(1)
while 1 != v:
me = b[v//32] & (1 << (v & 0x1f))
nx = nb[v//32] & (1 << (v & 0x1f))
if me != nx:
draw_cell(v, display, alive if nx else dead)
v = xorshift12(v)
draw_cell(0, display, alive if b[0//32] & (1 << (0 & 0x1f)) else dead)