1
0
mirror of https://github.com/osmarks/random-stuff synced 2025-06-07 12:54:05 +00:00

change things in some way

This commit is contained in:
osmarks 2025-05-18 14:09:06 +01:00
parent d057781e3c
commit f32abdf5b6
12 changed files with 672 additions and 45 deletions

10
.gitignore vendored
View File

@ -13,3 +13,13 @@ code-guessing/analytics/people
*/counts-old.csv */counts-old.csv
*/*.png */*.png
*.zst *.zst
tacnet.sqlite3
basic
basic.png
complex
complex.png
minecraft_images
comparisons.jsonl
nouns_cache.db
nouns.json
*.scad

View File

@ -63,6 +63,6 @@ This comes with absolutely no guarantee of support or correct function, although
* `bmp280_prometheus.py` - read BMP280 temperature/pressure sensors and export as Prometheus metrics. * `bmp280_prometheus.py` - read BMP280 temperature/pressure sensors and export as Prometheus metrics.
* `captive_portal.py` - automatically fill in captive portal forms (WIP). * `captive_portal.py` - automatically fill in captive portal forms (WIP).
* `scd4x_prometheus.py` - read SCD4x temperature/humidity/CO2 sensors and export as Prometheus metrics. * `scd4x_prometheus.py` - read SCD4x temperature/humidity/CO2 sensors and export as Prometheus metrics.
* `weight_painter.py` - paint arbitrary images into neural network weight matrices. Uses a permutation, so the distribution is preserved so training dynamics remain unaffected (so long as the network doesn't care about organization below the weight matrix level - this is not safe for attention heads etc). * `weight_painter.py` - paint arbitrary images into neural network weight matrices. Uses a permutation, so the distribution is preserved so training dynamics should not be affected much.
* `cool-effect.glsl` - a nice effect I made by accident whilst trying to make a cooler one. * `cool-effect.glsl` - a nice effect I made by accident whilst trying to make a cooler one.
* `portable_monitor_wallmount.py` - very simple CADQuery script which generates a frame for my portable monitor's top/bottom bezels so it can be stuck to the wall and slid out easily. * `portable_monitor_wallmount.py` - very simple CADQuery script which generates a frame for my portable monitor's top/bottom bezels so it can be stuck to the wall and slid out easily.

View File

@ -1,7 +1,7 @@
class Primes: class Primes:
def __init__(self, max): def __init__(self, max):
self.internal = range(2,max+1) self.internal = range(2,max+1)
def __next__(self): def __next__(self):
i = self.internal.__iter__().__next__() i = self.internal.__iter__().__next__()
self.internal = filter(lambda n : n % i != 0, self.internal) self.internal = filter(lambda n : n % i != 0, self.internal)
@ -10,4 +10,4 @@ class Primes:
def __iter__(self): return self def __iter__(self): return self
for i in Primes(100): for i in Primes(100):
print(i) print(i)

260
autocrafter.py Normal file
View File

@ -0,0 +1,260 @@
from dataclasses import dataclass
from collections import Counter, defaultdict, deque
import math
from typing import Generator
import scipy.optimize as opt
import numpy as np
import graphviz
@dataclass
class Recipe:
slots: list[str]
quantity: int
processing: bool
short_names = {
"minecraft:stone": "st",
"minecraft:redstone": "re",
"minecraft:glass": "gl",
"minecraft:glass_pane": "gp",
"minecraft:gold_ingot": "gi",
"minecraft:iron_ingot": "ii",
"minecraft:oak_wood": "ow",
"minecraft:oak_planks": "op",
"minecraft:oak_chest": "oc",
"computercraft:computer": "cc",
"computercraft:computer_advanced": "ca",
"computercraft:turtle": "tu",
"minecraft:birch_wood": "bw",
"minecraft:birch_planks": "bp",
"quark:birch_chest": "bc",
"computercraft:turtle_advanced": "ta",
"minecraft:gold_block": "gb",
"minecraft:coal": "co",
"minecraft:charcoal": "ch",
"minecraft:gold_ore": "go",
"minecraft:iron_ore": "io",
"minecraft:sand": "sa",
None: "-"
}
short_names_inv = {v: k for k, v in short_names.items()}
recipes = {}
def recipe_short(output, qty, inputs, processing=False):
recipes[short_names_inv[output]] = Recipe([short_names_inv.get(slot, slot) for slot in inputs.split()], qty, processing)
recipe_short("gp", 16, "gl gl gl gl gl gl")
recipe_short("op", 4, "ow")
recipe_short("oc", 1, "op op op op - op op op op")
recipe_short("cc", 1, "st st st st re st st gp st")
recipe_short("tu", 1, "ii ii ii ii cc ii ii oc ii")
recipe_short("ca", 1, "gi gi gi gi re gi gi gp gi")
recipe_short("ta", 1, "gi gi gi gi ca gi gi oc gi")
@dataclass
class Inventory:
contents: dict[str, int]
def __getitem__(self, item):
return self.contents.get(item, 0)
def add(self, item, quantity):
new_inventory = self.contents.copy()
new_inventory[item] = self.contents.get(item, 0) + quantity
return Inventory(new_inventory)
def take(self, item, quantity):
return self.add(item, -quantity)
class NoRecipe(BaseException):
pass
def solve(item: str, quantity: int, inventory: Inventory, use_callback, craft_callback) -> Inventory:
directly_available = min(inventory[item], quantity) # Consume items from storage if available
if directly_available > 0:
use_callback(item, directly_available)
inventory = inventory.take(item, directly_available)
quantity -= directly_available
if quantity > 0:
if recipe := recipes.get(item):
recipe_runs = math.ceil(quantity / recipe.quantity)
for citem, cquantity in Counter(recipe.slots).items():
if citem is not None:
inventory = solve(citem, recipe_runs * cquantity, inventory, use_callback, craft_callback) # Recurse into subrecipe
craft_callback(recipe, item, recipe_runs)
inventory = inventory.add(item, recipe_runs * recipe.quantity - quantity) # Add spare items to tracked inventory
else:
raise NoRecipe(item, quantity) # We need to make this and can't
return inventory
final = solve("computercraft:turtle", 1, Inventory({
"minecraft:stone": 100,
"minecraft:redstone": 1,
"minecraft:iron_ingot": 10,
"minecraft:oak_wood": 2,
"minecraft:glass": 7
}), lambda item, quantity: print(f"Using {quantity} {item}"), lambda recipe, item, runs: print(f"Crafting {runs}x{recipe.quantity} {item}"))
print(final)
def compute_item_graph(recipes_general):
recipes_forward_graph = defaultdict(set) # edge u→v exists where u is used to craft v
recipes_backward_graph = defaultdict(set) # edge u→v exists where v's recipe contains u
for src, recipes in recipes_general.items():
for recipe in recipes:
for input in Counter(recipe.slots).keys():
if input is not None:
recipes_forward_graph[input].add(src)
recipes_backward_graph[src].add(input)
return recipes_forward_graph, recipes_backward_graph
def item_graph(recipe_forward_graph, name):
dot = graphviz.Digraph("items", format="png", body=["\tpad=0.5\n"])
dot.attr("graph", nodesep="1", ranksep="1")
nodes = set()
def mk_node(item):
if not item in nodes:
dot.node(item.replace(":", "_"), "", image=f"minecraft_images/{item}.png", imagescale="true", shape="plaintext")
nodes.add(item)
for input, outputs in recipe_forward_graph.items():
mk_node(input)
for output in outputs:
dot.edge(input.replace(":", "_"), output.replace(":", "_"))
mk_node(output)
dot.render(filename=name)
recipes_general = defaultdict(list)
for src, recipe in recipes.items():
recipes_general[src].append(recipe)
item_graph(compute_item_graph(recipes_general)[0], "basic")
# Add multiple recipes for things to make the ILP solver work harder.
def recipe_short_extra(output, qty, inputs, processing=False):
recipes_general[short_names_inv[output]].append(Recipe([short_names_inv.get(slot, slot) for slot in inputs.split()], qty, processing))
recipe_short_extra("bp", 4, "bw")
recipe_short_extra("bc", 1, "bp bp bp bp - bp bp bp bp")
recipe_short_extra("tu", 1, "ii ii ii ii cc ii ii bc ii")
recipe_short_extra("gb", 1, "gi gi gi gi gi gi gi gi gi")
recipe_short("ta", 1, "gi gi gi gi ca gi gi bc gi")
recipe_short_extra("ta", 1, "gi gb gi gi tu gi - gi -")
recipe_short_extra("ta", 1, "gi gb gi gi tu gi - gi -")
for count in [1, 8]:
recipe_short_extra("gl", count, "ch" + " sa" * count, processing=True)
recipe_short_extra("gl", count, "co" + " sa" * count, processing=True)
recipe_short_extra("ii", count, "ch" + " io" * count, processing=True)
recipe_short_extra("ii", count, "co" + " io" * count, processing=True)
recipe_short_extra("gi", count, "ch" + " go" * count, processing=True)
recipe_short_extra("gi", count, "co" + " go" * count, processing=True)
recipe_short_extra("ch", count, "co" + " ow" * count, processing=True)
recipe_short_extra("ch", count, "co" + " bw" * count, processing=True)
recipes_forward_graph, recipes_backward_graph = compute_item_graph(recipes_general)
item_graph(recipes_forward_graph, "complex")
def topo_sort_inputs(item: str) -> Generator[str]:
seen = set()
# DFS to find root nodes in relevant segment (no incoming edges → no recipes)
def dfs(item):
if item in seen or not item: return
seen.add(item)
for input in recipes_backward_graph[item]:
dfs(input)
dfs(item)
roots = deque(item for item in seen if len(recipes_general[item]) == 0)
# Kahn's algorithm (approximately)
# Count incoming edges
counts = { item: len(inputs) for item, inputs in recipes_backward_graph.items() if item in seen }
while roots:
item = roots.popleft()
yield item
for out_edge in recipes_forward_graph[item]:
# Filter out items not in current operation's subtree
if (count := counts.get(out_edge)) != None:
counts[out_edge] -= 1
# It's now safe to use this item since its dependencies are all in output
if counts[out_edge] == 0:
roots.append(out_edge)
def solve_ilp(item: str, quantity: int, inventory: Inventory, use_callback, craft_callback):
sequence = list(topo_sort_inputs(item))
recipe_steps = []
# Rewrite (involved) recipes as production/consumption numbers.
items = { sitem: [] for sitem in sequence }
for item in sequence:
for recipe in recipes_general[item]:
step_changes = { sitem: 0 for sitem in sequence }
for citem, cquantity in Counter(recipe.slots).items():
if citem is not None:
step_changes[citem] = -cquantity
step_changes[item] = recipe.quantity
recipe_steps.append((item, recipe))
for sitem, coef in step_changes.items():
items[sitem].append(coef)
objective = np.ones(len(recipe_steps))
# The amount of each item we produce/consume is linearly dependent on how many times each recipe is executed.
# This matrix is that linear transform.
# Solver wants upper bounds so flip signs.
production_matrix = -np.stack([np.array(coefs) for item, coefs in items.items()])
# production_matrix @ x is the vector of item consumption, so we upper-bound that with inventory item counts
# and require that we produce the required output (negative net consumption)
item_constraint_vector = np.array([ -quantity + inventory[i] if i == item else inventory[i] for i in sequence ])
soln = opt.linprog(objective, integrality=np.ones_like(objective), A_ub=production_matrix, b_ub=item_constraint_vector)
match soln.status:
case 0:
print("OK")
# soln.x is now the number of times to execute each recipe_step
item_consumption = production_matrix @ soln.x
for item_name, consumption in zip(sequence, item_consumption):
consumption = int(consumption)
if consumption > 0:
use_callback(item_name, consumption)
inventory = inventory.take(item_name, consumption)
for (recipe_output, recipe_spec), execution_count in zip(recipe_steps, soln.x):
execution_count = int(execution_count)
if execution_count > 0:
craft_callback(recipe_spec, recipe_output, execution_count)
return inventory
case 1:
print("iteration limit reached")
raise NoRecipe
case 2:
print("infeasible")
raise NoRecipe
print(solve_ilp("computercraft:turtle_advanced", 1, Inventory({
"minecraft:stone": 100,
"minecraft:redstone": 1,
"minecraft:iron_ingot": 10,
"minecraft:gold_ore": 16,
"minecraft:oak_wood": 3,
"minecraft:birch_wood": 8,
"minecraft:glass": 7,
"minecraft:coal": 1,
"minecraft:sand": 16,
}), lambda item, quantity: print(f"Using {quantity} {item}"), lambda recipe, item, runs: print(f"Crafting {runs}x{recipe.quantity} {item}")))

25
bigram.py Normal file
View File

@ -0,0 +1,25 @@
from PIL import Image
import sys
from collections import defaultdict
import math
out = Image.new("RGB", (256, 256))
ctr = defaultdict(lambda: 0)
BS = 2<<18
with open(sys.argv[2], "rb") as f:
last = b""
while xs := f.read(BS):
for a, b in zip(last + xs, last + xs[1:]):
ctr[a, b] += 1
last = bytes([xs[-1]])
ctrl = { k: math.log(v) for k, v in ctr.items() }
maxv = max(ctrl.values())
for x, y in ctrl.items():
s = int(y / maxv * 255)
out.putpixel((x[0], x[1]), (0, s, 0))
out.save(sys.argv[1])

View File

@ -42,7 +42,7 @@ session = requests.Session()
DETECTPORTAL_URL = "http://detectportal.firefox.com/canonical.html" DETECTPORTAL_URL = "http://detectportal.firefox.com/canonical.html"
DETECTPORTAL_CONTENT = '<meta http-equiv="refresh" content="0;url=https://support.mozilla.org/kb/captive-portal"/>' DETECTPORTAL_CONTENT = '<meta http-equiv="refresh" content="0;url=https://support.mozilla.org/kb/captive-portal"/>'
PRIORITY_KEYWORDS = {"registr", "login", "signup", "signin"} PRIORITY_KEYWORDS = {"regist", "login", "signup", "signin"}
CONFIRM_SUFFIXES = {"2", "repeat", "confirm", "_repeat", "_confirm"} CONFIRM_SUFFIXES = {"2", "repeat", "confirm", "_repeat", "_confirm"}
EMAIL_BASE = "0t.lt" EMAIL_BASE = "0t.lt"
@ -85,11 +85,20 @@ def handle_response(response):
queue_ext = [] queue_ext = []
for link in soup.find_all("a"): for link in soup.find_all("a"):
if href := link.get("href"): if href := link.get("href"):
href = urllib.parse.urljoin(response.url, href)
if is_priority(href): if is_priority(href):
queue_ext.insert(0, href) queue_ext.insert(0, href)
else: else:
queue_ext.append(href) queue_ext.append(href)
"""
for script in soup.find_all("script"):
if src := script.get("src"):
queue_ext.append(src)
"""
for meta in soup.find_all("meta"):
if meta.get("http-equiv", "").lower() == "refresh":
if content := meta.get("content"):
if mat := re.match(r"\d+;URL='(.*)'", content):
queue_ext.append(mat.group(1))
for form in soup.find_all("form"): for form in soup.find_all("form"):
fields = {} fields = {}
@ -152,6 +161,7 @@ def handle_response(response):
response = session.post(action, data=fields) response = session.post(action, data=fields)
handle_response(response) handle_response(response)
queue_ext = [ urllib.parse.urljoin(response.url, q) for q in queue_ext ]
queue.extend(x for x in queue_ext if x not in tried) queue.extend(x for x in queue_ext if x not in tried)
while True: while True:

23
compare_things.py Normal file
View File

@ -0,0 +1,23 @@
import json
import random
with open("nouns.json", "r") as f:
nouns = set(json.load(f))
with open("comparisons.jsonl", "a") as f:
def writeline(obj):
f.write(json.dumps(obj, separators=(",", ":")) + "\n")
for noun in nouns:
other_noun = random.choice(list(nouns - {noun}))
print(noun, "/",other_noun)
pref = input("a/b/e/x/y: ")
writeline({"a": noun, "b": other_noun, "pref": pref})
if pref == "x":
writeline({"a": noun, "b": other_noun, "pref": "x"})
nouns.remove(noun)
elif pref == "y":
writeline({"a": other_noun, "b": noun, "pref": "y"})
nouns.remove(other_noun)
f.flush()

53
extract_nouns.py Normal file
View File

@ -0,0 +1,53 @@
import openai
import json
import os
import shelve
import re
import random
openai.api_key = os.environ["OPENAI_API_KEY"]
client = openai.OpenAI(api_key=os.environ["OPENAI_API_KEY"])
def chunks(text, size):
out = [""]
for line in text.split("\n"):
out[-1] += line + "\n"
if len(out[-1]) > size:
out.append("")
return [ x.removesuffix("\n") for x in out if x ]
def extract_nouns(text):
completion = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "user", "content": f"""Extract all unique simple noun phrases from this document and put them in a JSON array in the singular:
```
{text}
```"""}],
response_format={"type": "json_object"},
max_tokens=16384,
temperature=0.2 # should be 0 but repetition issues at 0
)
result = json.loads(completion.choices[0].message.content)
return result[next(iter(result.keys()))]
with open("../website/strings.json", "r") as f:
strings = json.load(f)
nouns = set()
with shelve.open("nouns_cache.db") as db:
for bigstring in strings:
for string in chunks(bigstring, 8192):
if nouns: print(random.choices(list(nouns), k=10))
if string in db:
nouns.update(db[string])
else:
print("reading:", string[:100])
s_nouns = extract_nouns(string)
nouns.update(s_nouns)
print(len(s_nouns), "/", len(nouns))
db[string] = s_nouns
with open("nouns.json", "w") as f:
json.dump(list(nouns), f)

View File

@ -1,9 +1,9 @@
import os, sys, subprocess, datetime import os, sys, subprocess, datetime
dt_threshold = datetime.datetime(2023, 6, 16).timestamp() dt_threshold = datetime.datetime(2024, 10, 22).timestamp()
_, indir, outdir = sys.argv _, indir, outdir = sys.argv
for x in os.listdir(indir): for x in sorted(os.listdir(indir)):
inpath = os.path.join(indir, x) inpath = os.path.join(indir, x)
if os.stat(inpath).st_mtime > dt_threshold: if os.stat(inpath).st_mtime > dt_threshold:
if subprocess.run(("feh", inpath)).returncode == 0: if subprocess.run(("feh", inpath)).returncode == 0:

18
parametric_mesh.py Normal file
View File

@ -0,0 +1,18 @@
from solid2 import *
import numpy as np
THICKNESS = 3
XYSIZE = 58
HOLE_SIZE = 4
LINE_SIZE = 0.5
LINE_SPACING = HOLE_SIZE + LINE_SIZE
XYSIZE = (XYSIZE // LINE_SPACING) * LINE_SPACING + LINE_SIZE
print(XYSIZE)
xlines = [ cube(LINE_SIZE, XYSIZE, THICKNESS).translate(x, 0, 0) for x in np.arange(0, XYSIZE, LINE_SPACING) ]
ylines = [ cube(XYSIZE, LINE_SIZE, THICKNESS).translate(0, y, 0) for y in np.arange(0, XYSIZE, LINE_SPACING) ]
model = union()(*xlines).union()(*ylines)
model.save_as_scad()

View File

@ -99,7 +99,10 @@ def clean_html(html):
remove_unknown_tags=True, remove_unknown_tags=True,
safe_attrs_only=True safe_attrs_only=True
) )
return cleaner.clean_html(feedparser.sanitizer._sanitize_html(html.replace("<!doctype html>", ""), "utf-8", "text/html")) try:
return cleaner.clean_html(feedparser.sanitizer._sanitize_html(html.replace("<!doctype html>", ""), "utf-8", "text/html"))
except:
return "HTML parse error"
def email_to_html(emsg, debug_info=False): def email_to_html(emsg, debug_info=False):
if isinstance(emsg, Message): if isinstance(emsg, Message):

299
wfc.html
View File

@ -1,40 +1,140 @@
<!DOCTYPE html> <!DOCTYPE html>
<canvas id="canv"></canvas> <style>
<input type="file" id="file"> canvas {
<button id="step">Step</button> border: 1px solid black;
}
#colors .color {
display: inline-block;
width: 20px;
height: 20px;
margin: 5px;
border: 1px solid black;
}
</style>
<canvas id="output"></canvas>
<canvas id="pattern"></canvas>
<div id="controls">
<button id="step">Step</button>
<button id="run">Run</button>
<button id="reset">Reset</button>
<input type="checkbox" id="hard-borders" checked><label for="hard-borders">Hard borders</label>
<input type="checkbox" id="ignore-directions" checked><label for="ignore-directions">Ignore directions</label>
<input type="checkbox" id="diagonals" checked><label for="diagonals">Diagonals</label>
</div>
<div id="colors"></div>
<div id="out"></div> <div id="out"></div>
<script> <script>
const write = line => { const write = line => {
const out = document.querySelector("#out") const out = document.querySelector("#out")
out.innerHTML = ""
out.appendChild(document.createTextNode(line)) out.appendChild(document.createTextNode(line))
out.appendChild(document.createElement("br")) out.appendChild(document.createElement("br"))
} }
const PX = 16 const PATTERN_W = 6
const W = 16 const W = 16
const RANGE = 10 const PX = 24
const ctx = document.querySelector("canvas").getContext("2d") const PATTERN_PX = 64
ctx.canvas.width = PX * W
ctx.canvas.height = PX * W
let grid = Array(W * W).fill(null).map(x => ({ value: null, options: new Set(Array(RANGE).fill(null).map((_, ix) => ix)) }))
const map = ([x, y]) => x + y * W const map = ([x, y]) => x + y * W
const unmap = a => [a % W, Math.floor(a / W)] const unmap = a => [a % W, Math.floor(a / W)]
const vsum = ([a, b], [c, d]) => [a + c, b + d] const vsum = ([a, b], [c, d]) => [a + c, b + d]
const adj = [[0, 1], [0, -1], [1, 0], [-1, 0]] const inRange = ([x, y]) => x >= 0 && x < W && y >= 0 && y < W
const inRange = p => p >= 0 && p < grid.length
const modclamp = x => x < 0 ? 10 + x : x % 10 const modclamp = x => x < 0 ? 10 + x : x % 10
let currentConstraintSet = {}
let colorsInUse = new Set()
let ignoreDirections = false
const readIgnoreDirections = () => ignoreDirections = document.querySelector("#ignore-directions").checked
readIgnoreDirections()
document.querySelector("#ignore-directions").addEventListener("change", readIgnoreDirections)
let hardBorders = false
const readHardBorders = () => hardBorders = document.querySelector("#hard-borders").checked
readHardBorders()
document.querySelector("#hard-borders").addEventListener("change", readHardBorders)
const getAdjacent = diagonals => diagonals ? [[0, 1], [0, -1], [1, 0], [-1, 0], [1, 1], [1, -1], [-1, 1], [-1, -1]] : [[0, 1], [0, -1], [1, 0], [-1, 0]]
let adj = null
const ctx = document.querySelector("canvas#output").getContext("2d")
ctx.canvas.width = PX * W
ctx.canvas.height = PX * W
let grid = Array(W * W).fill(null).map(x => ({ value: null, options: new Set() }))
const joinConstraintKey = adjs => {
if (ignoreDirections) adjs.sort()
return adjs.join(",")
}
const regenerateOptionsAt = (coord) => {
const data = grid[map(coord)]
const slots = []
for (const a of adj) {
const n = vsum(a, coord)
if (inRange(n)) {
const value = grid[map(n)].value
slots.push(value === null ? undefined : value)
} else {
slots.push(null)
}
}
if (slots.every(x => x === undefined)) {
data.options = colorsInUse // no constraints apply - shortcut
return
}
const out = new Set()
let hasMatch = false
// N-way Cartesian product of slots
const go = (slots, acc) => {
if (slots.length === 0) {
for (const prod of acc) {
const ckey = joinConstraintKey(prod)
if (currentConstraintSet[ckey]) {
for (const val of currentConstraintSet[ckey]) {
out.add(val)
}
hasMatch = true
}
}
} else {
const [fst, ...rest] = slots
if (fst === undefined) {
return go(rest, Array.from(colorsInUse).concat(hardBorders ? [] : [null]).flatMap(x => acc.map(xs => [x].concat(xs))))
} else {
return go(rest, acc.map(xs => [fst].concat(xs)))
}
}
}
go(slots, [[]])
data.options = out
}
const regenerateOptions = () => {
for (let x = 0; x < W; x++) {
for (let y = 0; y < W; y++) {
regenerateOptionsAt([x, y])
}
}
}
const readAdjacent = () => {
adj = getAdjacent(document.querySelector("#diagonals").checked)
regenerateOptions()
}
readAdjacent()
document.querySelector("#diagonals").addEventListener("change", readAdjacent)
const updatePos = (pos, value) => { const updatePos = (pos, value) => {
grid[map(pos)].value = value grid[map(pos)].value = value
console.log(value)
grid[map(pos)].options = null grid[map(pos)].options = null
for (const a of adj) { for (const a of adj) {
const n = map(vsum(a, pos)) const n = vsum(a, pos)
if (inRange(n) && grid[n].value === null) { if (inRange(n) && grid[map(n)].value === null && grid[map(n)].options !== null) {
for (const offset of [-1, 0, 1, 2, -2, 3, -3]) { regenerateOptionsAt(n)
grid[n].options.delete(modclamp(offset + value))
}
} }
} }
} }
@ -49,56 +149,181 @@
bestPos.push(unmap(index)) bestPos.push(unmap(index))
} }
return [bestQty, bestPos] return [bestQty, bestPos]
}, [RANGE, []]) }, [colorsInUse.size + 1, []])
const render = grid => { const render = grid => {
for (let x = 0; x < W; x++) { for (let x = 0; x < W; x++) {
for (let y = 0; y < W; y++) { for (let y = 0; y < W; y++) {
const data = grid[map([x, y])] const data = grid[map([x, y])]
const level = data.options && Math.floor(data.options.size / RANGE * 255).toString(16).padStart(2, "0") const level = data.options && Math.floor((data.options.size + 1) / (colorsInUse.size + 1) * 255).toString(16).padStart(2, "0") || "00"
ctx.fillStyle = data.value !== null ? `#0000${Math.floor(data.value / RANGE * 255).toString(16).padStart(2, "0")}` : `#${level}${level}${level}` ctx.fillStyle = data.value !== null ? data.value : `#${level}${level}${level}`
ctx.fillRect(x * PX, y * PX, PX, PX) ctx.fillRect(x * PX, y * PX, PX, PX)
if (data.value === null) {
ctx.strokeStyle = "#000000"
ctx.beginPath()
ctx.moveTo(x * PX, y * PX)
ctx.lineTo((x + 1) * PX, (y + 1) * PX)
ctx.closePath()
ctx.stroke()
ctx.strokeStyle = null
}
} }
} }
} }
let recentColors = ["#ffffff", "#000000", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#00ffff", "#ff00ff"]
const recentColorsDiv = document.querySelector("#colors")
let currentColor = recentColors[0]
const pushColor = color => {
recentColors = [color].concat(recentColors.filter(x => x !== color))
if (recentColors.length > 10) {
recentColors.shift()
}
currentColor = color
updateRecentColors()
}
const updateRecentColors = () => {
recentColorsDiv.innerHTML = ""
for (const color of recentColors) {
recentColorsDiv.appendChild(document.createElement("div"))
recentColorsDiv.lastChild.classList.add("color")
recentColorsDiv.lastChild.style.backgroundColor = color
recentColorsDiv.lastChild.addEventListener("click", () => {
pushColor(color)
})
}
}
const handleCanvasMouse = ev => {
if ((ev.buttons & 1) !== 0) {
return
}
const [x, y] = [Math.floor(ev.offsetX / PX), Math.floor(ev.offsetY / PX)]
if (x < 0 || x >= W || y < 0 || y >= W) {
return
}
const coord = [x, y]
const data = grid[map(coord)]
if (data.value === null) {
pushColor(currentColor)
updatePos(coord, currentColor)
}
}
ctx.canvas.addEventListener("click", handleCanvasMouse)
ctx.canvas.addEventListener("mousemove", handleCanvasMouse)
const pick = arr => arr[Math.floor(Math.random() * arr.length)] const pick = arr => arr[Math.floor(Math.random() * arr.length)]
render(grid) render(grid)
step.onclick = () => { step.onclick = () => {
const [qty, pos] = findBestCandidates(grid) const [qty, pos] = findBestCandidates(grid)
if (qty === 0) { if (pos.length === 0) {
write("contradiction") write("Done.")
return return
} }
write(`${qty} options on ${pos.length} tiles`) if (qty === 0) {
write("Contradiction.")
return
}
write(`${qty} options on ${pos.length} tiles.`)
if (qty === 1) { if (qty === 1) {
write("resolving") write(`Resolving ${pos.length} tiles.`)
for (const p of pos) { for (const p of pos) {
const newValue = Array.from(grid[map(p)].options)[0] const newValue = Array.from(grid[map(p)].options)[0]
console.log(newValue)
updatePos(p, newValue) updatePos(p, newValue)
} }
} else { } else {
const p = pick(pos) const p = pick(pos)
console.log(p, map(p), grid[map(p)])
const newValue = pick(Array.from(grid[map(p)].options)) const newValue = pick(Array.from(grid[map(p)].options))
console.log(newValue)
updatePos(p, newValue) updatePos(p, newValue)
} }
render(grid) render(grid)
} }
window.file.oninput = ev => { let timer
const file = e.target.files[0] const runButton = document.querySelector("#run")
const url = URL.createObjectURL(file) runButton.addEventListener("click", () => {
const img = new Image() if (runButton.innerHTML === "Run") {
runButton.innerHTML = "Stop"
img.onload = function() { timer = setInterval(step.onclick, 100)
URL.revokeObjectURL(img.src) } else {
c.getContext("2d").drawImage(img, 0, 0) runButton.innerHTML = "Run"
clearInterval(timer)
}
})
const reset = () => {
grid = Array(W * W).fill(null).map(x => ({ value: null, options: new Set() }))
clearInterval(timer)
runButton.innerHTML = "Run"
render(grid)
regenerateOptions()
}
document.querySelector("#reset").addEventListener("click", reset)
const patternEditor = () => {
const map = ([x, y]) => x + y * PATTERN_W
const pattern = document.querySelector("canvas#pattern")
const ctx = pattern.getContext("2d")
let grid = Array(PATTERN_W * PATTERN_W).fill(null).map(x => ({ value: currentColor }))
const inRange = ([x, y]) => x >= 0 && x < PATTERN_W && y >= 0 && y < PATTERN_W
pattern.width = PATTERN_PX * PATTERN_W
pattern.height = PATTERN_PX * PATTERN_W
const constraintKeyFor = coord => {
let constraintKey = []
for (const a of adj) {
const n = vsum(a, coord)
if (inRange(n)) {
constraintKey.push(grid[map(n)].value)
} else {
constraintKey.push(null)
}
}
return joinConstraintKey(constraintKey)
} }
img.src = url const recomputeConstraintSet = () => {
currentConstraintSet = new Set()
colorsInUse = new Set()
for (let x = 0; x < PATTERN_W; x++) {
for (let y = 0; y < PATTERN_W; y++) {
const coord = [x, y]
const data = grid[map(coord)]
const constraintKey = constraintKeyFor(coord)
currentConstraintSet[constraintKey] ??= new Set()
currentConstraintSet[constraintKey].add(data.value)
colorsInUse.add(data.value)
}
}
regenerateOptions()
}
const handlePatternMouse = ev => {
if ((ev.buttons & 1) !== 0) {
return
}
const [x, y] = [Math.floor(ev.offsetX / PATTERN_PX), Math.floor(ev.offsetY / PATTERN_PX)]
if (x < 0 || x >= PATTERN_W || y < 0 || y >= PATTERN_W) {
return
}
ctx.fillStyle = currentColor
ctx.fillRect(PATTERN_PX * x, PATTERN_PX * y, PATTERN_PX, PATTERN_PX)
grid[map([x, y])].value = currentColor
recomputeConstraintSet()
}
pattern.addEventListener("click", handlePatternMouse)
pattern.addEventListener("mousemove", handlePatternMouse)
recomputeConstraintSet()
} }
</script>
updateRecentColors()
patternEditor()
</script>