a956b3efe5
...and move GameOfLife out to make space The gallery app needs too much memory to run from bytecode, and has to be frozen in micropython.zip instead Signed-off-by: Francesco Gazzetta <fgaz@fgaz.me>
258 lines
8.2 KiB
Python
258 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)
|