diff --git a/slime-exp/examples/rain.py b/slime-exp/examples/rain.py new file mode 100644 index 0000000..f6833b4 --- /dev/null +++ b/slime-exp/examples/rain.py @@ -0,0 +1,26 @@ +from slime.grid import Grid +from slime.rules import SimpleMapping, Movement, Combine + +grid = Grid(10, 10, wrap_y=True) +grid[0, 0, "state"] = 1 +grid[1, 1, "state"] = 1 +grid[2, 0, "state"] = 1 +grid[3, 1, "state"] = 1 +grid[4, 0, "state"] = 1 +grid[5, 1, "state"] = 1 +grid[6, 0, "state"] = 1 +grid[7, 1, "state"] = 1 +grid[8, 0, "state"] = 1 +grid[9, 1, "state"] = 1 + +# grid.rules.append(SimpleMapping("state", {0: 1, 1: 0})) +# grid.rules.append(Movement("state", 0, 1)) +grid.rules.append( + Combine(SimpleMapping("state", {0: 1, 1: 0}), Movement("state", 0, 1)) +) + +while True: + # grid.render_text("state") + grid.render_text(lambda cell: {0: "|", 1: "-"}[cell["state"]]) + input() + grid.step() diff --git a/slime-exp/src/slime/grid.py b/slime-exp/src/slime/grid.py new file mode 100644 index 0000000..2ce0060 --- /dev/null +++ b/slime-exp/src/slime/grid.py @@ -0,0 +1,118 @@ +from enum import Enum +from collections import defaultdict +from .rules import Rule + + +class Connected(Enum): + FOUR_WAYS = 4 + EIGHT_WAYS = 8 + + +class Grid: + def __init__( + self, + width, + height, + *, + connected=Connected.FOUR_WAYS, + wrap_x=False, + wrap_y=False, + default_value=dict, + ): + self.width = width + self.height = height + self.connected = connected + self.wrap_x = wrap_x + self.wrap_y = wrap_y + self.grid = defaultdict(default_value) + self.rules = [] + + def neighbors(self, x, y): + x1 = x - 1 + x2 = x + 1 + y1 = y - 1 + y2 = y + 1 + if x1 < 0: + x1 = self.width - 1 if self.wrap_x else None + if x2 >= self.width: + x2 = 0 if self.wrap_x else None + if y1 < 0: + y1 = self.height - 1 if self.wrap_y else None + if y2 >= self.height: + y2 = 0 if self.wrap_y else None + + if x1 is not None: + yield (x1, y) + if x2 is not None: + yield (x2, y) + if y1 is not None: + yield (x, y1) + if y2 is not None: + yield (x, y2) + if self.connected == Connected.EIGHT_WAYS: + if x1 is not None and y1 is not None: + yield (x1, y1) + if x1 is not None and y2 is not None: + yield (x1, y2) + if x2 is not None and y1 is not None: + yield (x2, y1) + if x2 is not None and y2 is not None: + yield (x2, y2) + + def __getitem__(self, item): + x, y, prop = item + if self.wrap_x: + x %= self.width + if self.wrap_y: + y %= self.height + if x < 0 or x >= self.width or y < 0 or y >= self.height: + raise IndexError + return self.grid[item][prop] + + def __setitem__(self, item, value): + x, y, prop = item + if self.wrap_x: + x %= self.width + if self.wrap_y: + y %= self.height + if x < 0 or x >= self.width or y < 0 or y >= self.height: + raise IndexError + self.grid[x, y][prop] = value + + def __iter__(self): + return iter(self.grid.items()) + + def render_text(self, prop_or_func): + if callable(prop_or_func): + func = prop_or_func + else: + func = lambda x: x[prop_or_func] + for y in range(self.height): + for x in range(self.width): + if (x, y) in self.grid: + print(func(self.grid[(x, y)]), end="") + else: + print(" ", end="") + print() + + def add_rule(self, rule): + self.rules.append(rule) + + def step(self): + new_grid = defaultdict(dict) + for (x, y), cell in self.grid.items(): + for rule in self.rules: + for update in rule.step(x, y, cell, self): + # TODO: two rules shouldn't be able to update the same cell + if update.x < 0 or update.x >= self.width: + if not self.wrap_x: + continue + else: + update.x %= self.width + if update.y < 0 or update.y >= self.height: + if not self.wrap_y: + continue + else: + update.y %= self.height + new_grid[update.x, update.y].update(update.data) + self.grid = new_grid diff --git a/slime-exp/src/slime/rules.py b/slime-exp/src/slime/rules.py new file mode 100644 index 0000000..4551f78 --- /dev/null +++ b/slime-exp/src/slime/rules.py @@ -0,0 +1,51 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Generator + + +@dataclass +class CellUpdate: + x: int + y: int + data: dict + + +class Rule(ABC): + @abstractmethod + def step( + self, x: int, y: int, cell: dict, grid: "Grid" + ) -> Generator[CellUpdate, None, None]: + pass + + +class SimpleMapping(Rule): + def __init__(self, prop, mapping): + self.mapping = mapping + self.prop = prop + + def step(self, x, y, cell, grid): + yield CellUpdate(x, y, {self.prop: self.mapping.get(cell.get(self.prop))}) + + +class Movement(Rule): + def __init__(self, prop, dx, dy): + self.prop = prop + self.dx = dx + self.dy = dy + + def step(self, x, y, cell, grid): + yield CellUpdate(x + self.dx, y + self.dy, cell) + + +class Combine(Rule): + def __init__(self, *rules): + self.rules = rules + + def step(self, x, y, cell, grid): + updates = list(self.rules[0].step(x, y, cell, grid)) + for rule in self.rules[1:]: + next_updates = [] + for upd in updates: + next_updates.extend(rule.step(upd.x, upd.y, upd.data, grid)) + updates = next_updates + yield from updates