Initial commit: make TUI tool
dkl9

dkl9 commited on 2025-333 22:07:38
Showing 1 changed files, with 194 additions and 0 deletions.

... ...
@@ -0,0 +1,194 @@
1
+(require hyrule [-> case unless])
2
+(import hyrule [dec inc])
3
+(import curses math random sys)
4
+
5
+(setv WIDTH None)
6
+(setv SHAPE-CHARS (list "#$%&*+/=?"))
7
+(setv COL-DEPTH 15)
8
+
9
+(defn gx [x] (if (isinstance x tuple) (get x 0) (lfor k x (gx k))))
10
+
11
+(defn gy [x] (if (isinstance x tuple) (get x 1) (lfor k x (gy k))))
12
+
13
+(defn in-bounds [x y w h] (and (>= x 0) (>= y 0) (< x w) (< y h)))
14
+
15
+(defn rotate [shape qta]
16
+    (sfor #(x y) shape #(
17
+        (+ (* (** -1 (// qta 2)) (% (inc qta) 2) x) (* -1 (** -1 (// qta 2)) (% qta 2) y))
18
+        (+ (* (** -1 (// qta 2)) (% (inc qta) 2) y) (* (** -1 (// qta 2)) (% qta 2) x)))))
19
+
20
+(defn translate [shape dx dy]
21
+    (sfor #(x y) shape #((+ x dx) (+ y dy))))
22
+
23
+(defn normalise [shape]
24
+    (translate shape (- (min (gx shape) :default 0)) (- (min (gy shape) :default 0))))
25
+
26
+(defn init-grid [w h]
27
+    (dfor x1 (range w) y1 (range h)
28
+        #(x1 y1) (sfor #(x2 y2) [#((dec x1) y1) #(x1 (dec y1)) #((inc x1) y1) #(x1 (inc y1))]
29
+            :if (in-bounds x2 y2 w h) #(x2 y2))))
30
+
31
+(defn draw [scr shape ch [bg " "] [w 0] [h 0]]
32
+    (if (isinstance scr curses.window)
33
+        (if shape (for [#(x y) shape] (scr.addch y x ch)) "")
34
+        (if shape (let [cg (or scr (lfor _ (range (or h (inc (max (gy shape)))))
35
+                (lfor _ (range (or w (inc (max (gx shape))))) bg)))]
36
+            (for [#(x y) shape] (setv (get cg y x) ch))
37
+            cg) scr)))
38
+
39
+(defn to-pbm [shapes]
40
+    (setv w (+ 2 (max (lfor s shapes (- (max (gx s)) (min (gx s)) -1)))))
41
+    (setv h (inc (sum (lfor s shapes (inc (- (max (gy s)) (min (gy s)) -1))))))
42
+    (setv header f"P1\n{w} {h}\n")
43
+    (+ header (.join "\n" (lfor s shapes
44
+        (.join "\n" (lfor r (draw "" (translate (normalise s) 1 1) "1" "0" w) (.join "" r)))))
45
+        "\n" (* "0" w) "\n"))
46
+
47
+(defn colour [t]
48
+    (defn ud [x] (let [y (-> x (% 0.5) (* 6) (min 1))]
49
+        (round (* COL-DEPTH (if (>= (% x 1) 0.5) (- 1 y) y)))))
50
+    (let [r (ud (+ t (/ 1 3))) g (ud t) b (ud (+ t (/ 2 3)))] f"{r} {g} {b}"))
51
+
52
+(defn to-ppm [shapes]
53
+    (setv w (+ 3 (max (lfor s shapes (max (gx s))))))
54
+    (setv h (+ 3 (max (lfor s shapes (max (gy s))))))
55
+    (setv header f"P3\n{w} {h}\n{COL-DEPTH}\n")
56
+    (setv grid (draw "" #{#(0 0)} "0 0 0" "0 0 0" w h))
57
+    (for [#(i s) (enumerate shapes)]
58
+        (setv grid (draw grid (translate s 1 1) (colour (/ i (len shapes))))))
59
+    (+ header (.join "\n" (lfor r grid (.join " " r))) "\n"))
60
+
61
+(defn nbhd [xy]
62
+    (lfor d [#(1 0) #(0 1) #(-1 0) #(0 -1)] #((+ (gx xy) (gx d)) (+ (gy xy) (gy d)))))
63
+
64
+(defn hop-shape [xy src dst]
65
+    (setv (get dst xy) #{})
66
+    (del (get src xy))
67
+    (for [sc (nbhd xy)]
68
+        (when (in sc dst) (.add (get dst xy) sc) (.add (get dst sc) xy))
69
+        (when (in sc src) (.discard (get src sc) xy))))
70
+
71
+(defn components [shape]
72
+    (setv pl (set (shape.keys)))
73
+    (setv comps [])
74
+    (setv comp [])
75
+    (setv fi 0)
76
+    (while pl
77
+        (if (>= fi (len comp))
78
+            (do (when comp (comps.append (set comp))) (setv comp [(pl.pop)]) (setv fi 0))
79
+            (do
80
+                (for [ap (nbhd (get comp fi))]
81
+                    (when (in ap pl) (comp.append ap) (pl.remove ap)))
82
+                (+= fi 1))))
83
+    comps)
84
+
85
+(defn equiv [s1 s2]
86
+    (let [ns2 (normalise s2)]
87
+        (any (lfor r (range 4) (not (.symmetric-difference (normalise (rotate s1 r)) ns2))))))
88
+
89
+(defn builder [stdscr]
90
+    (setv KND {
91
+        (ord "h") #(-1 0) (ord "t") #(0 1)
92
+        (ord "n") #(0 -1) (ord "s") #(1 0)
93
+        curses.KEY-LEFT #(-1 0) curses.KEY-DOWN #(0 1)
94
+        curses.KEY-UP #(0 -1) curses.KEY-RIGHT #(1 0)})
95
+    (stdscr.clear)
96
+    (setv epi 0)
97
+    (setv cursor #(0 0))
98
+    (setv k 0)
99
+    (setv message "")
100
+    (while (!= k (ord "q"))
101
+        (stdscr.refresh)
102
+        (when (in k KND)
103
+            (let [d (get KND k) w #((+ (gx cursor) (gx d)) (+ (gy cursor) (gy d)))]
104
+                (when (in-bounds (gx w) (gy w) WIDTH WIDTH) (setv cursor w))))
105
+        (case (chr k)
106
+            "r" (do
107
+                (when (in cursor (get shapes 0))
108
+                    (unless epi (setv epi (len shapes)) (shapes.append {})))
109
+                (setv rpc 0)
110
+                (while (and
111
+                    (< (len (get shapes epi)) WIDTH)
112
+                    (setx opts (list (sfor rxy (| (.keys (get shapes epi)) #{cursor}) nxy (nbhd rxy)
113
+                        :if (in nxy (get shapes 0)) nxy))))
114
+                    (setv cursor (random.choice opts))
115
+                    (hop-shape cursor (get shapes 0) (get shapes epi))
116
+                    (+= rpc 1))
117
+                (setv message f"randomly picked {rpc}/{WIDTH} tiles ({(len (get shapes epi))}, {opts})"))
118
+            "a" (unless epi (when (in cursor (get shapes 0))
119
+                (setv epi (len shapes))
120
+                (shapes.append {}))
121
+                (setv message f"started shape {epi}")))
122
+        (when epi
123
+            (when (in cursor (get shapes 0)) (hop-shape cursor (get shapes 0) (get shapes epi)))
124
+            (when (>= (len (get shapes epi)) WIDTH)
125
+                (setv message f"completed shape {epi}")
126
+                (when (.startswith (setx message (cond
127
+                    (let [MPW (-> WIDTH (math.sqrt) (* 2) (math.floor)) ns (normalise (get shapes epi))]
128
+                        (any (lfor p ns (>= (max (gx p) (gy p)) MPW))))
129
+                        "WRN: shape would be too elongated"
130
+                    (any (lfor s (cut shapes None -1) (equiv s (get shapes epi))))
131
+                        "WRN: shape would be a duplicate"
132
+                    (any (lfor c (components (get shapes 0)) (% (len c) WIDTH)))
133
+                        "ERR: shape would leave invalid-size parts"
134
+                    True message)) "ERR")
135
+                    (for [xy (list (get shapes epi))]
136
+                        (hop-shape xy (get shapes epi) (get shapes 0)))
137
+                    (shapes.pop))
138
+                (setv epi 0)
139
+                (for [xy (get shapes 0)]
140
+                    (when (= (len (get shapes 0 xy)) 1)
141
+                        (setv epi (len shapes))
142
+                        (shapes.append {})
143
+                        (setv opts [xy])
144
+                        (while (and
145
+                            (= (len opts) 1)
146
+                            (< (len (get shapes epi)) WIDTH))
147
+                            (setv cursor (get opts 0))
148
+                            (setx opts (list (get shapes 0 cursor)))
149
+                            (hop-shape cursor (get shapes 0) (get shapes epi)))
150
+                        (unless (message.startswith "ERR")
151
+                            (setv message f"inferred {(len (get shapes epi))} cells of forced shape {epi}"))
152
+                        (when (any (lfor comp (components (get shapes 0))
153
+                            (- (len (get shapes epi)) (% (len comp) WIDTH))))
154
+                            (setv message f"ERR: invalid-size parts forced?"))
155
+                        (break)))
156
+                (unless (or epi (not (get shapes 0)) (in cursor (get shapes 0)))
157
+                    (setv cursor (next (iter (lfor x (range WIDTH) y (range WIDTH)
158
+                        :if (in #(x y) (get shapes 0)) #(x y))))))))
159
+        (when (= (len (get shapes 0)) 0) (break))
160
+        (for [#(i s) (enumerate shapes)]
161
+            (draw stdscr s (get SHAPE-CHARS i)))
162
+        (stdscr.addch (gy cursor) (gx cursor) "@")
163
+        (stdscr.hline WIDTH 0 " " (get (stdscr.getmaxyx) 1))
164
+        (stdscr.addstr WIDTH 0
165
+            f"({(gx cursor)}, {(gy cursor)}) in poly {(try (get (lfor #(i s) (enumerate shapes) :if (in cursor s) i) 0) (except [IndexError] "?"))}{(if message (+ " | " message) "")}")
166
+        (setv k (stdscr.getch))))
167
+
168
+(defn load-shapes [fname]
169
+    (setv fl (with [fh (open fname "r")]
170
+        (lfor l (fh.readlines) :if (not (l.startswith "#")) (l.strip))))
171
+    (setv WIDTH (len (get fl 0)))
172
+    (setv shapes [(init-grid WIDTH WIDTH)])
173
+    (for [#(y r) (enumerate fl) #(x c) (enumerate r) :if (.isdigit c)]
174
+        (let [i (int c)]
175
+            (when (<= (len shapes) i) (shapes.extend (lfor _ (range (- (inc i) (len shapes))) {})))
176
+            (hop-shape #(x y) (get shapes 0) (get shapes i))))
177
+    shapes)
178
+ 
179
+(try
180
+    (setv mode (get sys.argv 1))
181
+    (setv fname (get sys.argv 2))
182
+    (cond
183
+        (.isdigit mode) (do
184
+            (setv WIDTH (int mode))
185
+            (setv shapes [(init-grid WIDTH WIDTH)])
186
+            (curses.wrapper builder)
187
+            (setv og (draw "" #{#(0 0)} "_" "_" WIDTH WIDTH))
188
+            (for [#(i s) (enumerate shapes)] (setv og (draw og s (str i) "_")))
189
+            (with [fh (open fname "w")] (fh.write (+ (.join "\n" (lfor r og (.join "" r))) "\n"))))
190
+        (= mode "q") (print (to-pbm (lfor s (load-shapes fname)
191
+            :if s (rotate s (random.randrange 4)))))
192
+        (= mode "a") (print (to-ppm (lfor s (load-shapes fname) :if s s))))
193
+    (except [FileNotFoundError]
194
+        (print f"{(get sys.argv 0)}: can't load from {fname}: No such file or directory")))
0 195