wasp-os/apps/GameOfLife.py

259 lines
8.2 KiB
Python

# SPDX-License-Identifier: LGPL-3.0-or-later
# Copyright (C) 2020 Daniel Thompson
"""Conway's Game of Life
~~~~~~~~~~~~~~~~~~~~~~~~
The Game of Life is a "no player game" played on a two dimensional grid
where the rules interact to make interesting patterns.
.. figure:: res/LifeApp.png
:width: 179
Screenshot of the Game of Life application
The game is based on four simple rules:
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.
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:
"""Convert a 12-bit number into a reasonably bright RGB565 pixel"""
rgb = v ^ (v << 4)
while 0 == (rgb & 0xc710):
rgb += 0x2104
return rgb
@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
In the code below we have simplified the rules described at the top
of this file 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@\xf8B\x02B\x02B?\x16L?\x15'
b'L?\x16B\x02B\x02B?\x1bB?\x1eD?\x1d'
b'D?\x1eB?\x17\x80\xee\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\x89\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(625)
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 = 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 = 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)