state machine

This commit is contained in:
David Allemang
2026-09-04 15:21:54 -04:00
commit 3149659af9
12 changed files with 1137 additions and 0 deletions

257
.gitignore vendored Normal file
View File

@@ -0,0 +1,257 @@
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### VirtualEnv template
# Virtualenv
# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
.Python
[Bb]in
[Ii]nclude
[Ll]ib
[Ll]ib64
[Ll]ocal
[Ss]cripts
pyvenv.cfg
.venv
pip-selfcheck.json
### Python template
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

3
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

View File

@@ -0,0 +1,52 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="HtmlUnknownTag" enabled="true" level="WARNING" enabled_by_default="true">
<option name="myValues">
<value>
<list size="12">
<item index="0" class="java.lang.String" itemvalue="nobr" />
<item index="1" class="java.lang.String" itemvalue="noembed" />
<item index="2" class="java.lang.String" itemvalue="comment" />
<item index="3" class="java.lang.String" itemvalue="noscript" />
<item index="4" class="java.lang.String" itemvalue="embed" />
<item index="5" class="java.lang.String" itemvalue="script" />
<item index="6" class="java.lang.String" itemvalue="my-paragraph" />
<item index="7" class="java.lang.String" itemvalue="mc-diorama" />
<item index="8" class="java.lang.String" itemvalue="mc-world" />
<item index="9" class="java.lang.String" itemvalue="mc-frame" />
<item index="10" class="java.lang.String" itemvalue="mc-timeline" />
<item index="11" class="java.lang.String" itemvalue="mc-camera" />
</list>
</value>
</option>
<option name="myCustomValuesEnabled" value="true" />
</inspection_tool>
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<list>
<option value="numpy" />
<option value="opencv-contrib-python-headless" />
<option value="joblib" />
<option value="loguru" />
<option value="matplotlib" />
<option value="pymatreader" />
<option value="tqdm" />
<option value="ipykernel" />
<option value="itk-montage" />
<option value="simpleitk" />
<option value="multiview-stitcher" />
<option value="itk-elastix" />
<option value="paraview" />
</list>
</option>
</inspection_tool>
<inspection_tool class="PyStubPackagesAdvertiser" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<list>
<option value="pandas" />
</list>
</option>
</inspection_tool>
</profile>
</component>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/wireless-formalism.iml" filepath="$PROJECT_DIR$/.idea/wireless-formalism.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

10
.idea/wireless-formalism.iml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.system.id="pyproject.toml" type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="~/src/wireless-formalism/.venv" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

509
finite_state.py Normal file
View File

@@ -0,0 +1,509 @@
import math
from contextlib import redirect_stdout
from dataclasses import dataclass
from itertools import islice
from typing import Tuple, Optional, List, Set
@dataclass(frozen=True)
class Entry:
priority: int
delay: int
name: str = '-'
def linear_parts(self) -> Tuple[int, ...]:
if self.delay == 0:
return ()
return (self.priority,) + (-9999,) * (self.delay - 1)
@dataclass(frozen=True)
class Transition:
from_state: str
to_state: str
entries: Tuple[Entry, ...]
def linearize(self) -> Tuple[int, ...]:
return tuple(p for e in self.entries for p in e.linear_parts())
def total_delay(self) -> int:
return sum(e.delay for e in self.entries)
class TilesetFSM:
def __init__(self, transitions: List[Transition], start_state: str, accept_states: Set[str]):
self.transitions = transitions
self.start_state = start_state
self.accept_states = accept_states
# Pre-group transitions by their source state for O(1) branching lookup
self.adj_list = {state: [] for state in set(t.from_state for t in transitions)}
for t in transitions:
self.adj_list[t.from_state].append(t)
def _check_divergence(self, prefix: Tuple[int, ...], target: Tuple[int, ...]) -> int:
"""
Returns -1 if prefix is lexicographically < target,
1 if prefix is lexicographically > target,
0 if prefix is a strict prefix of target (no divergence yet).
"""
for p_val, t_val in zip(prefix, target):
if p_val < t_val: return -1
if p_val > t_val: return 1
return 0
def find_next(self, current_lin: Tuple[int, ...], target_delay: int) -> Optional[List[Entry]]:
best_next_chain = None
best_next_lin = None
def dfs(chain_so_far: Tuple[Transition, ...], delay_so_far: int, current_state: str):
nonlocal best_next_chain, best_next_lin
# Base Case: Exact delay reached AND machine is in an accepting state
if delay_so_far == target_delay:
if current_state in self.accept_states:
lin = tuple(p for t in chain_so_far for p in t.linearize())
if lin > current_lin:
if best_next_lin is None or lin < best_next_lin:
best_next_lin = lin
best_next_chain = chain_so_far
return
if delay_so_far > target_delay:
return
p_lin = tuple(p for t in chain_so_far for p in t.linearize())
# Prefix Pruning
if self._check_divergence(p_lin, current_lin) == -1: return
if best_next_lin and self._check_divergence(p_lin, best_next_lin) == 1: return
for t in self.adj_list.get(current_state, []):
dfs(chain_so_far + (t,), delay_so_far + t.total_delay(), t.to_state)
dfs((), 0, self.start_state)
if best_next_chain:
return [entry for t in best_next_chain for entry in t.entries]
return None
def parse_names(self, names: List[str]) -> List[Entry]:
"""
Takes a sequence of component names and infers their priorities and delays
by walking the state machine.
"""
current_state = self.start_state
i = 0
inferred_chain = []
while i < len(names):
match_found = False
# Look at all valid transitions from our current state
for t in self.adj_list.get(current_state, []):
# Extract the names of the components in this transition
t_names = [e.name for e in t.entries]
# Check if this transition matches the next components in our input
if names[i : i + len(t_names)] == t_names:
inferred_chain.extend(t.entries)
current_state = t.to_state
i += len(t_names)
match_found = True
break
if not match_found:
raise ValueError(
f"Syntax Error: Cannot place '{names[i]}' while in state '{current_state}' "
f"at index {i}."
)
if current_state not in self.accept_states:
raise ValueError(
f"Unexpected EOF: Sequence ended in non-accepting state '{current_state}'. "
"Did you forget to un-invert a torch?"
)
return inferred_chain
# ==========================================
# 1. GENERATE THE CARTESIAN STATE MACHINE
# ==========================================
transitions = []
for signal in ["NORMAL", "INVERTED"]:
for faces in ["OTHER", "DIODE"]:
current_state = f"{signal}_{faces}"
# A. Wire (Zero-delay state reset. Breaks diode chains)
# FIX: Only allow wire if we are actually facing a diode.
if faces == "DIODE":
transitions.append(Transition(
from_state=current_state,
to_state=f"{signal}_OTHER",
entries=(Entry(0, 0, 'wire'),)
))
# B. Comparator
cmp_pri = -1 if faces == "DIODE" else 0
transitions.append(Transition(
from_state=current_state,
to_state=f"{signal}_DIODE",
entries=(Entry(cmp_pri, 2, 'cmp'),)
))
# C. Repeaters
for d in (2, 4, 6, 8):
if faces == "DIODE":
rep_pri = -3
else:
rep_pri = -1 if signal == "NORMAL" else -2
transitions.append(Transition(
from_state=current_state,
to_state=f"{signal}_DIODE",
entries=(Entry(rep_pri, d, f'rep{d}'),)
))
# D. Torch (Inverts signal, is not a diode)
next_sig = "INVERTED" if signal == "NORMAL" else "NORMAL"
transitions.append(Transition(
from_state=current_state,
to_state=f"{next_sig}_OTHER",
entries=(Entry(0, 2, 'torch'),)
))
# E. Fluids (Only allowed on rising edges/NORMAL)
if signal == "NORMAL":
for d in (5, 10, 30):
fluid_macro = (Entry(0, 2, 'obs'), Entry(1, d, f'fluid{d}'), Entry(0, 4, 'disp'))
transitions.append(Transition(
from_state=current_state,
to_state="NORMAL_OTHER",
entries=fluid_macro
))
# Initialize assuming the end of the chain faces nothing (OTHER).
# We require the final accepting state to be NORMAL (un-inverted) to be valid.
model = TilesetFSM(
transitions=transitions,
start_state='NORMAL_OTHER',
accept_states={'NORMAL_OTHER', 'NORMAL_DIODE'}
)
# ==========================================
# 2. USAGE EXAMPLES
# ==========================================
def display_chain(entries):
# Print the chain, highlighting the name and generated priority
return " <- ".join([f"{e.name}({e.priority})" for e in entries])
def linearize_entries(entries):
return tuple(p for e in entries for p in e.linear_parts())
# if __name__ == "__main__":
# # Example 1: The Zero-Delay Wire breaking the context
# chain_names_1 = ["cmp", "wire", "cmp"]
# entries_1 = model.parse_names(chain_names_1)
#
# print(f"Input: {chain_names_1}")
# print(f"Inferred: {display_chain(entries_1)}")
# # Output: cmp(0) -> wire(0) -> cmp(0)
# print()
#
# # Example 2: Diode-facing-diode context
# chain_names_2 = ["cmp", "cmp"]
# entries_2 = model.parse_names(chain_names_2)
#
# print(f"Input: {chain_names_2}")
# print(f"Inferred: {display_chain(entries_2)}")
# # Output: cmp(0) -> cmp(-1)
# print()
#
# # Example 3: Falling-edge inversion behavior
# chain_names_3 = ["torch", "rep4", "wire", "rep4", "torch"]
# entries_3 = model.parse_names(chain_names_3)
#
# print(f"Input: {chain_names_3}")
# print(f"Inferred: {display_chain(entries_3)}")
# # Output: torch(0) -> rep4(-3) -> wire(0) -> rep4(-2) -> torch(0)
# # (Notice the first rep4 faces the torch (a diode), getting -3,
# # while the second faces the wire (OTHER) during an inverted state, getting -2)
# print()
#
# # Example 4: Compiling a sequence to use as a search target
# target_names = ["cmp", "cmp", "cmp", "cmp"]
# target_entries = model.parse_names(target_names)
# target_lin = linearize_entries(target_entries)
#
# print(f"Finding NEXT chain for target: {target_names}")
# print(f"Inferred: {display_chain(target_entries)}")
# next_chain = model.find_next(target_lin, target_delay=8)
# if next_chain:
# print(f"Next: {display_chain(next_chain)}")
# else:
# print(f'Next: None')
# # Output: Next: rep2(-1) -> rep2(-3) -> rep2(-3) -> rep2(-3)
def enumerate_chains(model: TilesetFSM, target_delay: int):
"""
Yields every valid tile sequence of the target delay in lexicographical order.
"""
# Bootstrap with an infinitely low sequence to find the very first chain
current_lin = (-999,) * target_delay
while True:
# Find the next valid sequence
next_chain = model.find_next(current_lin, target_delay)
# If no next sequence exists, we've enumerated the entire language
if next_chain is None:
break
yield next_chain
# Update our pointer for the next iteration
current_lin = linearize_entries(next_chain)
# ==========================================
# USAGE EXAMPLE
# ==========================================
# if __name__ == "__main__":
# target_delay = 6
# print(f"Enumerating all valid chains of Delay {target_delay}...\n")
#
# chain_count = 0
# for chain in enumerate_chains(model, target_delay):
# chain_count += 1
#
# # We can extract the names to see the raw component sequence
# names = [e.name for e in chain]
# print(f"{chain_count: 3d}. {names}")
#
# print(f"Total sequences found for delay {target_delay}: {chain_count}")
import heapq
from typing import List
def generate_render_document(chain_entries: List['Entry']) -> str:
"""
Generates a setblock-style animation document for a given redstone chain.
"""
# Reverse chain so x=0 is the first component to activate (upstream)
components = chain_entries[::-1]
N = len(components)
# 1. Forward sweep to calculate resting states (t=0)
resting_power = False
resting_states = []
for c in components:
if c.name == 'torch':
# Torches invert the resting power for downstream components
resting_power = not resting_power
resting_states.append(resting_power)
elif c.name == 'obs':
# Observers block resting power
resting_power = False
resting_states.append(False)
else:
# rep, cmp, and wire pass resting power
resting_states.append(resting_power)
lines = []
actual_visual_state = {}
posmap = {}
x = 0
for i, (c, resting) in enumerate(zip(components, resting_states)):
is_powered = "true" if resting else "false"
if c.name == 'wire':
lines.append(f"p {x} 0 0 white_concrete")
lines.append(f"p {x} -1 0 smooth_stone_slab type=top")
elif c.name == 'torch':
lines.append(f"p {x} 0 0 white_concrete")
lines.append(f"p {x} -1 0 smooth_stone_slab type=top")
x += 1
lines.append(f"p {x} 0 0 redstone_wall_torch facing=east lit={is_powered}")
lines.append(f"p {x} -1 0 smooth_stone_slab type=top")
actual_visual_state[(i, 'lit')] = is_powered
posmap[i] = x
elif c.name.startswith('rep'):
delay_ticks = int(c.name[3:]) // 2
lines.append(f"p {x} 0 0 repeater facing=west powered={is_powered} locked=false delay={delay_ticks}")
lines.append(f"p {x} -1 0 smooth_stone_slab type=top")
actual_visual_state[(i, 'powered')] = is_powered
posmap[i] = x
elif c.name == 'cmp':
lines.append(f"p {x} 0 0 comparator facing=west powered={is_powered} mode=compare")
lines.append(f"p {x} -1 0 smooth_stone_slab type=top")
actual_visual_state[(i, 'powered')] = is_powered
posmap[i] = x
elif c.name == 'obs':
lines.append(f"p {x} 0 0 observer facing=west powered=false")
lines.append(f"p {x} -1 0 smooth_stone_slab type=top")
actual_visual_state[(i, 'powered')] = "false"
posmap[i] = x
x += 1
# 4. Simulate a 2gt pulse passing through the system
# pq holds (time, component_index, input_level)
pq = []
heapq.heappush(pq, (0, 0, True))
heapq.heappush(pq, (2, 0, False))
visual_changes = {} # time -> dict of (index, prop) -> value
while pq:
t, i, level = heapq.heappop(pq)
if i >= N:
continue
c = components[i]
if c.name == 'wire':
# Instantly passes the signal to the next component
heapq.heappush(pq, (t, i+1, level))
elif c.name == 'torch':
out_level = not level
out_t = t + 2
heapq.heappush(pq, (out_t, i+1, out_level))
val_str = "true" if out_level else "false"
if out_t not in visual_changes: visual_changes[out_t] = {}
visual_changes[out_t][(i, 'lit')] = val_str
elif c.name.startswith('rep'):
delay = int(c.name[3:])
out_t = t + delay
heapq.heappush(pq, (out_t, i+1, level))
val_str = "true" if level else "false"
if out_t not in visual_changes: visual_changes[out_t] = {}
visual_changes[out_t][(i, 'powered')] = val_str
elif c.name == 'cmp':
out_t = t + 2
heapq.heappush(pq, (out_t, i+1, level))
val_str = "true" if level else "false"
if out_t not in visual_changes: visual_changes[out_t] = {}
visual_changes[out_t][(i, 'powered')] = val_str
elif c.name == 'obs':
# An observer fires a 2gt pulse whenever its input changes
out_t_on = t + 2
out_t_off = t + 4
heapq.heappush(pq, (out_t_on, i+1, True))
heapq.heappush(pq, (out_t_off, i+1, False))
if out_t_on not in visual_changes: visual_changes[out_t_on] = {}
if out_t_off not in visual_changes: visual_changes[out_t_off] = {}
# Python's heapq pops `False` before `True` if times match.
# This perfectly replicates observer pulse-extension: if an OFF and ON
# hit on the exact same tick, the ON overwrites the OFF.
visual_changes[out_t_on][(i, 'powered')] = "true"
visual_changes[out_t_off][(i, 'powered')] = "false"
# 5. Format the dynamic animation frames
sorted_times = sorted(visual_changes.keys())
for t in sorted_times:
frame_lines = []
changes_at_t = visual_changes[t]
for (i, prop), val in changes_at_t.items():
x = posmap[i]
# Only emit if it actually changes the visual state
if actual_visual_state.get((i, prop)) != val:
actual_visual_state[(i, prop)] = val
frame_lines.append(f"p {x} 0 0 {prop}={val}")
if frame_lines:
lines.append(f"t {t}")
lines.extend(frame_lines)
# 6. Add final padding frame
last_t = sorted_times[-1] if sorted_times else 0
lines.append(f"t {last_t + 4}")
lines.append("p 0 0 0")
return "\n".join(lines)
def makedoc(chain: list[Entry], z=0):
x = 0
for el in chain:
match el.name:
case 'cmp':
yield f'p {x} 0 {z} comparator facing=west powered=false mode=compare'
yield f'p {x} -1 {z} smooth_stone_slab type=top'
case 'rep2':
yield f'p {x} 0 {z} repeater facing=west powered=false locked=false delay=1'
yield f'p {x} -1 {z} smooth_stone_slab type=top'
case 'rep4':
yield f'p {x} 0 {z} repeater facing=west powered=false locked=false delay=2'
yield f'p {x} -1 {z} smooth_stone_slab type=top'
case 'rep6':
yield f'p {x} 0 {z} repeater facing=west powered=false locked=false delay=3'
yield f'p {x} -1 {z} smooth_stone_slab type=top'
case 'rep8':
yield f'p {x} 0 {z} repeater facing=west powered=false locked=false delay=4'
yield f'p {x} -1 {z} smooth_stone_slab type=top'
case 'wire':
yield f'p {x} 0 {z} white_concrete'
yield f'p {x} -1 {z} smooth_stone_slab type=top'
case 'torch':
yield f'p {x} 0 {z} redstone_wall_torch facing=east lit=false'
yield f'p {x} -1 {z} smooth_stone_slab type=top'
x -= 1
yield f'p {x} 0 {z} white_concrete'
yield f'p {x} -1 {z} smooth_stone_slab type=top'
x -= 1
# return ' '.join(el.name for el in chain)
# for el in chain:
# match el.name:
# print(el.name)
if __name__ == "__main__":
target_delay = 6
with open('/home/local/KHQ/david.allemang/src/wireless/content/tilesets.typ', 'w') as f:
with redirect_stdout(f):
print('''
#import "/lib.typ": diorama, example, note, todo
== Tilesets <tilesets>
#show raw.where(lang: "mc-diorama"): it => diorama(
zoom: true,
theta: 180,
phi: 60,
radius: 30,
height: "120em",
it.text,
)
''')
z = 0
print('```mc-diorama')
for chain in enumerate_chains(model, target_delay):
for line in makedoc(chain, z):
print(line)
z -= 1.25
print('```')

169
grammar.py Normal file
View File

@@ -0,0 +1,169 @@
from dataclasses import dataclass
from typing import Tuple, Optional, List
@dataclass(frozen=True)
class Entry:
priority: int
delay: int
name: str = '-'
def linear_parts(self) -> Tuple[int, ...]:
return (self.priority,) * self.delay
# A ComponentBlock is an atomic sequence of Entries that can be physically built.
ComponentBlock = Tuple[Entry, ...]
def block_linearize(block: ComponentBlock) -> Tuple[int, ...]:
"""Linearizes a single macro-block."""
return tuple(p for e in block for p in e.linear_parts())
def chain_linearize(chain: Tuple[ComponentBlock, ...]) -> Tuple[int, ...]:
"""Linearizes a full sequence of blocks."""
return tuple(p for b in chain for p in block_linearize(b))
def flatten_chain(chain: Tuple[ComponentBlock, ...]) -> List[Entry]:
"""Converts the internal tuple-based chain back to your flat List[Entry] format."""
return [entry for block in chain for entry in block]
class TilesetModel:
def __init__(self, allowed_blocks: List[ComponentBlock]):
self.blocks = allowed_blocks
def _check_divergence(self, prefix: Tuple[int, ...], target: Tuple[int, ...]) -> int:
"""
Returns -1 if prefix is lexicographically < target,
1 if prefix is lexicographically > target,
0 if prefix is a strict prefix of target (no divergence yet).
"""
for p_val, t_val in zip(prefix, target):
if p_val < t_val: return -1
if p_val > t_val: return 1
return 0
def find_next(self, current_chain: Tuple[ComponentBlock, ...]) -> Optional[Tuple[ComponentBlock, ...]]:
"""Finds the lexicographically next chain that perfectly matches the total delay."""
target_delay = sum(e.delay for b in current_chain for e in b)
current_lin = chain_linearize(current_chain)
best_next_chain = None
best_next_lin = None
def dfs(chain_so_far: Tuple[ComponentBlock, ...], delay_so_far: int):
nonlocal best_next_chain, best_next_lin
# Base Case: Exact delay reached
if delay_so_far == target_delay:
lin = chain_linearize(chain_so_far)
if lin > current_lin:
if best_next_lin is None or lin < best_next_lin:
best_next_lin = lin
best_next_chain = chain_so_far
return
# Base Case: Overshot delay limits
if delay_so_far > target_delay:
return
p_lin = chain_linearize(chain_so_far)
# PRUNING 1: If prefix diverges and is strictly LESS than current_lin,
# any suffix appended to it will also be strictly less. Prune the branch.
if self._check_divergence(p_lin, current_lin) == -1:
return
# PRUNING 2: If prefix diverges and is strictly GREATER than the best_next_lin
# we've already found, any suffix will also be greater. It can't beat our current best.
if best_next_lin is not None:
if self._check_divergence(p_lin, best_next_lin) == 1:
return
# Branching
for b in self.blocks:
dfs(chain_so_far + (b,), delay_so_far + sum(e.delay for e in b))
dfs((), 0)
return best_next_chain
def find_previous(self, current_chain: Tuple[ComponentBlock, ...]) -> Optional[Tuple[ComponentBlock, ...]]:
"""Finds the lexicographically previous chain matching total delay."""
target_delay = sum(e.delay for b in current_chain for e in b)
current_lin = chain_linearize(current_chain)
best_prev_chain = None
best_prev_lin = None
def dfs(chain_so_far: Tuple[ComponentBlock, ...], delay_so_far: int):
nonlocal best_prev_chain, best_prev_lin
if delay_so_far == target_delay:
lin = chain_linearize(chain_so_far)
if lin < current_lin:
if best_prev_lin is None or lin > best_prev_lin:
best_prev_lin = lin
best_prev_chain = chain_so_far
return
if delay_so_far > target_delay:
return
p_lin = chain_linearize(chain_so_far)
# Prune if prefix diverges and is strictly GREATER than current_lin
if self._check_divergence(p_lin, current_lin) == 1:
return
# Prune if prefix diverges and is strictly LESS than best_prev_lin
if best_prev_lin is not None:
if self._check_divergence(p_lin, best_prev_lin) == -1:
return
for b in self.blocks:
dfs(chain_so_far + (b,), delay_so_far + sum(e.delay for e in b))
dfs((), 0)
return best_prev_chain
# --- Define the "Alphabet" of valid Macro-Blocks ---
# Standard Components
repeaters = [(Entry(p, d, f'rep({p},{d})'),) for p in (-1, -3) for d in (2, 4, 6, 8)]
comparators = [(Entry(p, 2, f'cmp({p})'),) for p in (0, -1)]
others = [(Entry(0, 2, 'other'),)]
# Subsequences: Signal Inverted Repeaters
# (Note: Updates flow right-to-left. Torch2 updates latest so it goes on the left)
inv_repeaters = [
(Entry(0, 2, 'torch2'), Entry(p, d, f'inv_rep({p},{d})'), Entry(0, 2, 'torch1'))
for p in (-2, -3) for d in (2, 4, 6, 8)
]
# Subsequences: Fluids & Observers
# (Observer updates latest so it goes on the left)
fluids = [
(Entry(0, 2, 'obs'), Entry(1, d, f'fluid({d})'), Entry(0, 4, 'disp'))
for d in (5, 10, 30)
]
# Compile the generative grammar
ALL_MACRO_BLOCKS = repeaters + comparators + others + inv_repeaters + fluids
# --- Usage Example ---
model = TilesetModel(ALL_MACRO_BLOCKS)
# Let's say we have a specific chain of Total Delay = 14
current = (
(Entry(0, 2, 'cmp(0)'),),
(Entry(-3, 8, 'rep(-3,8)'),),
(Entry(-1, 4, 'rep(-1,4)'),)
)
print(f"Current Chain: {flatten_chain(current)}")
next_chain = model.find_next(current)
if next_chain:
print(f"Next Chain: {flatten_chain(next_chain)}")
prev_chain = model.find_previous(current)
if prev_chain:
print(f"Prev Chain: {flatten_chain(prev_chain)}")

104
permute.py Normal file
View File

@@ -0,0 +1,104 @@
from dataclasses import dataclass
from pprint import pprint
from random import shuffle
from typing import Iterable
# Here we model tilesets as "chains"; sequences of named components and their priority-delay pairs.
# Components are processed via a priority queue; so on each tick, components which are scheduled to update will be processed in priority order.
# When components update, they trigger the following component to update after some delay. Then, in the future, that component is updated according to its priority and the process continues down the chain.
# If two components update in the same tick with the same priority, then they do so in the order in which they were scheduled.
# Note that components which update later in time have stronger significance on the order of execution; so for our notation we place these on the left-hand-side (first in lists) to match left-to-right lexicographic notation.
@dataclass
class Entry:
priority: int
delay: int
name: str = '-'
# We can model this by linearizing these tuples into tuples, then lexicographically sort these linearized keys.
# We directly sort on priority: more-negative priorities update before more-positive priorities, so these are correctly sorted lexicographically. However we only want this behavior in the case that the components update at the same time, so we can 'pad out' the effect of delay by repeating the priority.
def linear_parts(entry: Entry):
assert entry.delay > 0
# return (-np.inf,) * (entry.delay - 1) + (entry.priority,)
return (entry.priority,) * entry.delay
def linearize(entries: Iterable[Entry]):
return [part for entry in entries for part in linear_parts(entry)]
def sort(chains: list[Entry]):
return sorted(chains, key=linearize)
# one framework to produce a tileset is to encode binary values by comparator and repeater. all cases have equal delay, so it just falls to the priorities. comparators have priority 0 at the end and -1 in the middle, while repeaters have priority -1 at the end and -2 in the middle.
chains = [
[Entry(-0, 2, 'cmp'), Entry(-1, 2, 'cmp'), Entry(-1, 2, 'cmp'), Entry(-1, 2, 'cmp')],
[Entry(-0, 2, 'cmp'), Entry(-1, 2, 'cmp'), Entry(-1, 2, 'cmp'), Entry(-2, 2, 'rep')],
[Entry(-0, 2, 'cmp'), Entry(-1, 2, 'cmp'), Entry(-2, 2, 'rep'), Entry(-1, 2, 'cmp')],
[Entry(-0, 2, 'cmp'), Entry(-1, 2, 'cmp'), Entry(-2, 2, 'rep'), Entry(-2, 2, 'rep')],
[Entry(-0, 2, 'cmp'), Entry(-3, 2, 'rep'), Entry(-1, 2, 'cmp'), Entry(-1, 2, 'cmp')],
[Entry(-0, 2, 'cmp'), Entry(-3, 2, 'rep'), Entry(-1, 2, 'cmp'), Entry(-3, 2, 'rep')],
[Entry(-0, 2, 'cmp'), Entry(-3, 2, 'rep'), Entry(-3, 2, 'rep'), Entry(-1, 2, 'cmp')],
[Entry(-0, 2, 'cmp'), Entry(-3, 2, 'rep'), Entry(-3, 2, 'rep'), Entry(-3, 2, 'rep')],
[Entry(-1, 2, 'rep'), Entry(-1, 2, 'cmp'), Entry(-1, 2, 'cmp'), Entry(-1, 2, 'cmp')],
[Entry(-1, 2, 'rep'), Entry(-1, 2, 'cmp'), Entry(-1, 2, 'cmp'), Entry(-3, 2, 'rep')],
[Entry(-1, 2, 'rep'), Entry(-1, 2, 'cmp'), Entry(-3, 2, 'rep'), Entry(-1, 2, 'cmp')],
[Entry(-1, 2, 'rep'), Entry(-1, 2, 'cmp'), Entry(-3, 2, 'rep'), Entry(-3, 2, 'rep')],
[Entry(-1, 2, 'rep'), Entry(-3, 2, 'rep'), Entry(-1, 2, 'cmp'), Entry(-1, 2, 'cmp')],
[Entry(-1, 2, 'rep'), Entry(-3, 2, 'rep'), Entry(-1, 2, 'cmp'), Entry(-3, 2, 'rep')],
[Entry(-1, 2, 'rep'), Entry(-3, 2, 'rep'), Entry(-3, 2, 'rep'), Entry(-1, 2, 'cmp')],
[Entry(-1, 2, 'rep'), Entry(-3, 2, 'rep'), Entry(-3, 2, 'rep'), Entry(-3, 2, 'rep')],
]
shuffle(chains)
# another framework is to use permutations of repeaters on varying delay. now all cases have equal priority, so it just falls to the delays.
chains = [
[Entry(-1, 8, 'r4'), Entry(-3, 6, 'r3'), Entry(-3, 4, 'r2'), Entry(-3, 2, 'r1')],
[Entry(-1, 8, 'r4'), Entry(-3, 6, 'r3'), Entry(-3, 2, 'r1'), Entry(-3, 4, 'r2')],
[Entry(-1, 8, 'r4'), Entry(-3, 4, 'r2'), Entry(-3, 6, 'r3'), Entry(-3, 2, 'r1')],
[Entry(-1, 8, 'r4'), Entry(-3, 4, 'r2'), Entry(-3, 2, 'r1'), Entry(-3, 6, 'r3')],
[Entry(-1, 8, 'r4'), Entry(-3, 2, 'r1'), Entry(-3, 6, 'r3'), Entry(-3, 4, 'r2')],
[Entry(-1, 8, 'r4'), Entry(-3, 2, 'r1'), Entry(-3, 4, 'r2'), Entry(-3, 6, 'r3')],
[Entry(-1, 6, 'r3'), Entry(-3, 8, 'r4'), Entry(-3, 4, 'r2'), Entry(-3, 2, 'r1')],
[Entry(-1, 6, 'r3'), Entry(-3, 8, 'r4'), Entry(-3, 2, 'r1'), Entry(-3, 4, 'r2')],
[Entry(-1, 6, 'r3'), Entry(-3, 4, 'r2'), Entry(-3, 8, 'r4'), Entry(-3, 2, 'r1')],
[Entry(-1, 6, 'r3'), Entry(-3, 4, 'r2'), Entry(-3, 2, 'r1'), Entry(-3, 8, 'r4')],
[Entry(-1, 6, 'r3'), Entry(-3, 2, 'r1'), Entry(-3, 8, 'r4'), Entry(-3, 4, 'r2')],
[Entry(-1, 6, 'r3'), Entry(-3, 2, 'r1'), Entry(-3, 4, 'r2'), Entry(-3, 8, 'r4')],
[Entry(-1, 4, 'r2'), Entry(-3, 8, 'r4'), Entry(-3, 6, 'r3'), Entry(-3, 2, 'r1')],
[Entry(-1, 4, 'r2'), Entry(-3, 8, 'r4'), Entry(-3, 2, 'r1'), Entry(-3, 6, 'r3')],
[Entry(-1, 4, 'r2'), Entry(-3, 6, 'r3'), Entry(-3, 8, 'r4'), Entry(-3, 2, 'r1')],
[Entry(-1, 4, 'r2'), Entry(-3, 6, 'r3'), Entry(-3, 2, 'r1'), Entry(-3, 8, 'r4')],
[Entry(-1, 4, 'r2'), Entry(-3, 2, 'r1'), Entry(-3, 8, 'r4'), Entry(-3, 6, 'r3')],
[Entry(-1, 4, 'r2'), Entry(-3, 2, 'r1'), Entry(-3, 6, 'r3'), Entry(-3, 8, 'r4')],
[Entry(-1, 2, 'r1'), Entry(-3, 8, 'r4'), Entry(-3, 6, 'r3'), Entry(-3, 4, 'r2')],
[Entry(-1, 2, 'r1'), Entry(-3, 8, 'r4'), Entry(-3, 4, 'r2'), Entry(-3, 6, 'r3')],
[Entry(-1, 2, 'r1'), Entry(-3, 6, 'r3'), Entry(-3, 8, 'r4'), Entry(-3, 4, 'r2')],
[Entry(-1, 2, 'r1'), Entry(-3, 6, 'r3'), Entry(-3, 4, 'r2'), Entry(-3, 8, 'r4')],
[Entry(-1, 2, 'r1'), Entry(-3, 4, 'r2'), Entry(-3, 8, 'r4'), Entry(-3, 6, 'r3')],
[Entry(-1, 2, 'r1'), Entry(-3, 4, 'r2'), Entry(-3, 6, 'r3'), Entry(-3, 8, 'r4')],
]
shuffle(chains)
# However neither of these are the general case.
#
# Basically, we want a framework that can take a particular chain of (priority, delay) pairs and come up with the *next* (or previous) sequence given this `linearize` ordering using the (priority, delay) pairs which we can generate.
#
# the full list of generable pairs is described as follows.
#
# repeaters: [-1,-3] x [2,4,6,8]
# comparators: [-0,-1] x [2]
# all other components: [-0] x [2]
#
# it is in principle possible to generate sub-sequences of other tuples, however these require involved setup.
#
# by using redstone torches to invert a signal, it is possible to generate a subsequence involving priority -2. (While the signal is inverted, repeaters generate priorities [-2,-3] x [2,4,6,8])
# [(-0,2), (-2,2), (-0,2)]
#
# by using fluids and observers, it is possible to generate signals of delay 5, 30 (in the overworld) or delay 10 only (in the nether). fluids tick after tile ticks, so it is effectively a priority of +1.
# [(-0,2), (+1,5), (-0,4)] # the sequence here is a dispenser (-0,4) produces water (+1,5) which is observed by observer (-0,2).
# pprint([tuple(el.name for el in chain) for chain in chains])
pprint([tuple(el.name for el in chain) for chain in sort(chains)])

5
pyproject.toml Normal file
View File

@@ -0,0 +1,5 @@
[project]
name = "wireless-formalism"
version = "0.1.0"
requires-python = ">=3.14"
dependencies = []

8
uv.lock generated Normal file
View File

@@ -0,0 +1,8 @@
version = 1
revision = 3
requires-python = ">=3.14"
[[package]]
name = "wireless-formalism"
version = "0.1.0"
source = { virtual = "." }