diff --git a/Makefile b/Makefile index f8b63d3..007486d 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ BUILD_DIR := build EXAMPLES_DIR := examples # Define all available examples (add new ones here) -EXAMPLES := wit-bottle flask-app +EXAMPLES := wit-bottle flask-app game-of-life # Default example for serve target EXAMPLE ?= wit-bottle diff --git a/examples/README.md b/examples/README.md index 7463394..07601c6 100644 --- a/examples/README.md +++ b/examples/README.md @@ -14,12 +14,16 @@ This directory contains example applications demonstrating different approaches - **Features**: Flask routing, request handling, error handling - **Use Case**: More complex applications, familiar Flask patterns +### `game-of-life.py` +A server-side implementation of Conway’s Game of Life, with a server round trip per frame. This demonstrates raw requests-per-second performance. + ## Building and Running Examples ### Build a Specific Example ```bash make build/flask-app.wasm # Build Flask example make build/wit-bottle.wasm # Build Bottle example +make build/game-of-life.wasm # Build Conway's Game of Life example ``` ### Serve an Example diff --git a/examples/game-of-life.py b/examples/game-of-life.py new file mode 100644 index 0000000..a7ce80d --- /dev/null +++ b/examples/game-of-life.py @@ -0,0 +1,266 @@ +from base64 import urlsafe_b64decode +from random import random + +from flask import Flask + +try: + from fastly_compute.wsgi import WsgiHttpIncoming +except ImportError: + # We're running this using the Flask debug server. + running_under_compute = False +else: + running_under_compute = True + + +app = Flask(__name__) + + +# Board width and height. We assume a square board for now. +# 45 crashes. Viceroy will pass us no more than 1936 bytes of the board. (Or +# maybe the entire URL gets truncated at 1965b.) If you change this, change the +# f"{i:010000b}" format string below to be the new value squared. +WIDTH = HEIGHT = 50 + + +def decompressed_board(compressed: str) -> str: + """Decompress the board representation sent from JS, returning a B&W board + string ("10011011"...). + + :arg compressed: A urlsafe_b64encode()d representation of the bit-packed + black-and-white board. (We don't need color info in order to compute the + next board.) + + This saves 83% space. RLE would save about 90% but would depend on the board + and would be slower to compute. + """ + if compressed == "none": + return "none" + i = int.from_bytes(urlsafe_b64decode(compressed)) + return f"{i:02500b}" + + +@app.route("/board/") +def board(compressed_board: str): + """Return the next frame of the Game Of Life, given the current one. If a "" + board is given, return a new random board. + """ + cells = decompressed_board(compressed_board) + + # Random board on start: + if cells == "none": + return "".join("1" if random() < 0.1 else "0" for _ in range(HEIGHT * WIDTH)) + + # Otherwise, evolve 1 step: + new_cells = "" + for i in range(len(cells)): + new_cells += new_cell_color(cells, i) + return new_cells + + +def new_cell_color(cells, cell_index): + """Compute the new color of a single cell at the given offset.""" + count = sum( + (cells[neighbor_index] != "0") for neighbor_index in neighbors(cell_index) + ) + if cells[cell_index] != "0": + if 2 <= count <= 3: + return str(count) + else: + return "0" + elif count == 3: + return "1" + return "0" + + +def xy_neighbors(x, y): + """Return an iterable of the x-y coordinates of all the adjacent cells, + omitting any that are outside the board bounds.""" + x_can_grow = x + 1 < WIDTH + y_can_grow = y + 1 < HEIGHT + x_can_shrink = x >= 1 + y_can_shrink = y >= 1 + if x_can_grow: + yield x + 1, y + if x_can_shrink: + yield x - 1, y + if y_can_grow: + yield x, y + 1 + if y_can_shrink: + yield x, y - 1 + if x_can_grow and y_can_grow: + yield x + 1, y + 1 + if x_can_grow and y_can_shrink: + yield x + 1, y - 1 + if x_can_shrink and y_can_grow: + yield x - 1, y + 1 + if x_can_shrink and y_can_shrink: + yield x - 1, y - 1 + + +def neighbors(cell_index): + """Return an iterable of the indices of all the adjacent cells, omitting any + that are outside the board bounds.""" + y, x = divmod(cell_index, WIDTH) + for neighbor_x, neighbor_y in xy_neighbors(x, y): + yield neighbor_y * WIDTH + neighbor_x + + +@app.route("/") +def root(): + return """ + + + + + Conway's Game of Life + + + + +

FPS

+ + + +""" % {"width": WIDTH, "viewbox_width": 10 + WIDTH * 10} + + +if running_under_compute: + HttpIncoming = WsgiHttpIncoming(app) diff --git a/pyproject.toml b/pyproject.toml index b97c4ab..8617b33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,7 @@ select = [ ] ignore = [ "E501", # line too long, handled by formatter + "UP031", # % string formatting, minimally disruptive for stdlib-based HTML templating ] [tool.ruff.format]