Added curated list of rule/seed combinations to WolframAutomata for use when user...
[screensavers] / hacks / WolframAutomata / WolframAutomata.c
CommitLineData
0c731d4a
AT
1/* (c) 2021 Aaron Taylor <ataylor at subgeniuskitty dot com> */
2/* See LICENSE.txt file for copyright and license details. */
3
4
5/* TODO: Write description explaining that this simulates all 1D NN CAs, and explain briefly what all those terms imply. */
6/* TODO: Explain things like the topology of the space. */
7/* TODO: Explain how the numbering for a CA expands to the actual rules. */
8/* TODO: Briefly explain the four different classes of behavior and their implications. */
9/* TODO: Include a link to Wikipedia. */
10/* TODO: I suppose a lot of this stuff goes in the README instead. */
11/* TODO: Explain the data structures in detail. */
12/* TODO: Explain all the options, like the various starting conditions. */
13
14
15/* TODO: Check manpage for all functions I use and ensure my includes are correct. I don't want to depend on picking up includes via screenhack.h. */
16/* TODO: Verify everything in this file is C89. Get rid of things like '//' comments, pack all my declarations upfront, no stdint, etc. */
0c731d4a
AT
17
18#include "screenhack.h"
19
20// Command line options
7ce88c8e 21// directory to output XBM files of each run (and call an external command to convert to PNGs?)
2b742550 22// -save-dir STRING
7ce88c8e 23// number of generations to simulate
2b742550 24// -num-generations N
7ce88c8e 25// delay time (speed of simulation)
2b742550 26// -delay-usec N
7ce88c8e 27// foreground and background color
2b742550 28// ??? (strings of some sort, but I need to look up what X resources to interact with)
7ce88c8e 29// display info overlay with CA number and start conditions?
2b742550 30// -overlay
7ce88c8e 31// which ruleset number to use? Or random? Or random from small set of hand-selected interesting examples?
80cfe219 32// In order of precedence:
14d68c5b 33// -rule-random (select a random rule on each run)
80cfe219
AT
34// -rule N (always simulate Rule N on each run)
35// (if neither of the above two are specified, then a random CURATED rule is selected on each run)
14d68c5b
AT
36// which starting population to use, random or one bit? (for random: allow specifying a density)
37// In order of precedence:
38// -population-single
39// -population-random DENSITY
40// (the two options above only apply to the simulation under the -rule-random or -rule N options. in curated mode, starting population is defined in the curation array)
41// TODO: In the future, add the option for user to pass list of cell IDs to turn ON.
2b742550
AT
42// size of pixel square (e.g. 1x1, 2x2, 3x3, etc)
43// -pixel-size N
0c731d4a 44
14d68c5b
AT
45/* -------------------------------------------------------------------------- */
46/* Data Structures */
47/* -------------------------------------------------------------------------- */
48
0c731d4a 49struct state {
7ce88c8e
AT
50 /* Various X resources */
51 Display * dpy;
52 Window win;
53 GC gc;
54
55 // TODO: Explain that this holds the whole evolution of the CA and the actual displayed visualization is simply a snapshot into this pixmap.
56 Pixmap evolution_history;
7ce88c8e
AT
57
58 // TODO: Explain all of these.
7ce88c8e
AT
59 unsigned long fg, bg;
60 int xlim, ylim, ypos; // explain roughly how and where we use these. Note: I'm not thrilled xlim/ylim since they are actually the width of the display, not the limit of the index (off by one). Change those names.
61 Bool display_info;
7ce88c8e
AT
62
63 Bool * current_generation;
80cfe219
AT
64
65 // TODO: Describe these.
66 uint8_t rule_number; // Note: This is not a CLI option. You're thinking of rule_requested.
67 uint8_t rule_requested; // Note: Repurposing Rule 0 as a null value.
68 Bool rule_random;
c428f3d5 69
14d68c5b
AT
70 // TODO: Describe these.
71 int population_density;
72 Bool population_single;
73
c428f3d5
AT
74 /* Misc Commandline Options */
75 int pixel_size; /* Size of CA cell in pixels (e.g. pixel_size=3 means 3x3 pixels per cell). */
76 int delay_microsec; /* Requested delay to screenhack framework before next call to WolframAutomata_draw(). */
7969381e 77 int num_generations; /* Number of generations of the CA to simulate before restarting. */
c428f3d5
AT
78
79 /* Expository Variables - Not strictly necessary, but makes some code easier to read. */
80 size_t number_of_cells;
0c731d4a
AT
81};
82
14d68c5b
AT
83// TODO: Decorations
84enum seed_population {
1f5d1274
AT
85 random_cell,
86 middle_cell,
87 edge_cell
14d68c5b
AT
88};
89
90// TODO: Decorations
91struct curated_ruleset {
92 uint8_t rule;
93 enum seed_population seed;
94};
95
1f5d1274 96// TODO: Decorations
14d68c5b 97static const struct curated_ruleset curated_ruleset_list[] = {
1f5d1274
AT
98 {18, middle_cell},
99 {30, middle_cell},
100 {45, middle_cell},
101 {54, middle_cell},
102 {57, middle_cell},
103 {73, middle_cell},
104 {105, middle_cell},
105 {109, middle_cell},
106 {129, middle_cell},
107 {133, middle_cell},
108 {135, middle_cell},
109 {150, middle_cell},
110 {30, edge_cell},
111 {45, edge_cell},
112 {57, edge_cell},
113 {60, edge_cell},
114 {75, edge_cell},
115 {107, edge_cell},
116 {110, edge_cell},
117 {133, edge_cell},
118 {137, edge_cell},
119 {169, edge_cell},
120 {225, edge_cell},
121 {22, random_cell},
122 {30, random_cell},
123 {54, random_cell},
124 {62, random_cell},
125 {90, random_cell},
126 {105, random_cell},
127 {108, random_cell},
128 {110, random_cell},
129 {126, random_cell},
130 {146, random_cell},
131 {150, random_cell},
132 {182, random_cell},
133 {184, random_cell},
134 {225, random_cell},
135 {240, random_cell}
80cfe219
AT
136};
137
14d68c5b
AT
138/* -------------------------------------------------------------------------- */
139/* Helper Functions */
140/* -------------------------------------------------------------------------- */
141
142// TODO: decorations? inline?
143void
144generate_random_seed(struct state * state)
145{
146 int i;
147 for (i = 0; i < state->number_of_cells; i++) {
148 state->current_generation[i] = ((random() % 100) < state->population_density) ? True : False;
149 }
150}
151
152// TODO: function decorations?
153// TODO: Explain why this santizes the index for accessing current_generation (i.e. it creates a circular topology).
154size_t
155sindex(struct state * state, int index)
156{
157 while (index < 0) {
158 index += state->number_of_cells;
159 }
160 while (index >= state->number_of_cells) {
161 index -= state->number_of_cells;
162 }
163 return (size_t) index;
164}
165
166// TODO: function decorations?
167// TODO: At least give a one-sentence explanation of the algorithm since this function is the core of the simulation.
168Bool
169calculate_cell(struct state * state, int cell_id)
170{
171 uint8_t cell_pattern = 0;
172 int i;
173 for (i = -1; i < 2; i++) {
174 cell_pattern = cell_pattern << 1;
175 if (state->current_generation[sindex(state, cell_id+i)] == True) {
176 cell_pattern |= 1;
177 }
178 }
179 if ((state->rule_number >> cell_pattern) & 1) {
180 return True;
181 } else {
182 return False;
183 }
184}
185
186// TODO: function decorations?
187void
188render_current_generation(struct state * state)
189{
190 size_t xpos;
191 for (xpos = 0; xpos < state->number_of_cells; xpos++) {
192 if (state->current_generation[xpos] == True) {
193 XFillRectangle(state->dpy, state->evolution_history, state->gc, xpos*state->pixel_size, state->ypos, state->pixel_size, state->pixel_size);
194 }
195 }
196}
197
198/* -------------------------------------------------------------------------- */
199/* Screenhack API Functions */
200/* -------------------------------------------------------------------------- */
201
0c731d4a
AT
202static void *
203WolframAutomata_init(Display * dpy, Window win)
204{
7ce88c8e
AT
205 struct state * state = calloc(1, sizeof(*state)); // TODO: Check calloc() call
206 XGCValues gcv;
207 XWindowAttributes xgwa;
14d68c5b 208 const struct curated_ruleset * curated_ruleset = NULL;
7ce88c8e
AT
209
210 state->dpy = dpy;
211 state->win = win;
212
213 XGetWindowAttributes(state->dpy, state->win, &xgwa);
214 state->xlim = xgwa.width;
215 state->ylim = xgwa.height;
216 state->ypos = 0; // TODO: Explain why.
217
218 state->fg = gcv.foreground = get_pixel_resource(state->dpy, xgwa.colormap, "foreground", "Foreground");
219 state->bg = gcv.background = get_pixel_resource(state->dpy, xgwa.colormap, "background", "Background");
220 state->gc = XCreateGC(state->dpy, state->win, GCForeground, &gcv);
221
c428f3d5 222 state->delay_microsec = get_integer_resource(state->dpy, "delay-usec", "Integer");
7ce88c8e
AT
223 if (state->delay_microsec < 0) state->delay_microsec = 0;
224
c428f3d5
AT
225 state->pixel_size = get_integer_resource(state->dpy, "pixel-size", "Integer");
226 if (state->pixel_size < 1) state->pixel_size = 1;
227 if (state->pixel_size > state->xlim) state->pixel_size = state->xlim;
228
229 state->number_of_cells = state->xlim / state->pixel_size;
14d68c5b 230 // TODO: Do we want to enforce that number_of_cells > 0?
c428f3d5 231
80cfe219
AT
232 /* The minimum number of generations is 2 since we must allocate enough */
233 /* space to hold the seed generation and at least one pass through */
234 /* WolframAutomata_draw(), which is where we check whether or not we've */
235 /* reached the end of the pixmap. */
7969381e
AT
236 state->num_generations = get_integer_resource(state->dpy, "num-generations", "Integer");
237 if (state->num_generations < 0) state->num_generations = 2;
238
80cfe219
AT
239 /* Time to figure out which rule to use for this simulation. */
240 /* We ignore any weirdness resulting from the following cast since every */
241 /* bit pattern is also a valid rule; if the user provides weird input, */
242 /* then we'll return weird (but well-defined!) output. */
243 state->rule_requested = (uint8_t) get_integer_resource(state->dpy, "rule-requested", "Integer");
244 state->rule_random = get_boolean_resource(state->dpy, "rule-random", "Boolean");
245 /* Through the following set of branches, we enforce CLI flag precedence. */
246 if (state->rule_random) {
247 /* If this flag is set, the user wants truly random rules rather than */
248 /* random rules from a curated list. */
249 state->rule_number = (uint8_t) random();
250 } else if (state->rule_requested != 0) {
251 /* Rule 0 is terribly uninteresting, so we are reusing it as a 'null' */
252 /* value and hoping nobody notices. Finding a non-zero value means */
253 /* the user requested a specific rule. Use it. */
254 state->rule_number = state->rule_requested;
255 } else {
256 /* No command-line options were specified, so select rules randomly */
257 /* from a curated list. */
14d68c5b
AT
258 size_t number_of_array_elements = sizeof(curated_ruleset_list)/sizeof(curated_ruleset_list[0]);
259 curated_ruleset = &curated_ruleset_list[random() % number_of_array_elements];
260 state->rule_number = curated_ruleset->rule;
261 }
262
263 /* Time to construct the seed generation for this simulation. */
264 state->population_single = get_boolean_resource(state->dpy, "population-single", "Boolean");
265 state->population_density = get_integer_resource(state->dpy, "population-density", "Integer");
266 if (state->population_density < 0 || state->population_density > 100) state->population_density = 50;
267 state->current_generation = calloc(1, sizeof(*state->current_generation)*state->number_of_cells);
268 if (!state->current_generation) {
269 fprintf(stderr, "ERROR: Failed to calloc() in WolframAutomata_init().\n");
270 exit(EXIT_FAILURE);
271 }
272 if (curated_ruleset) {
273 /* If we're using a curated ruleset, ignore any CLI flags related to */
274 /* setting the seed generation, instead drawing that information from */
275 /* the curated ruleset. */
276 switch (curated_ruleset->seed) {
1f5d1274
AT
277 case random_cell: generate_random_seed(state); break;
278 case middle_cell: state->current_generation[state->number_of_cells/2] = True; break;
279 case edge_cell : state->current_generation[0] = True; break;
14d68c5b
AT
280 }
281 } else {
282 /* If we're not using a curated ruleset, process any relevant flags */
283 /* from the user, falling back to a random seed generation if nothing */
284 /* else is specified. */
285 if (state->population_single) {
286 state->current_generation[0] = True;
287 } else {
288 generate_random_seed(state);
289 }
80cfe219
AT
290 }
291
7ce88c8e
AT
292 // TODO: These should be command-line options, but I need to learn how the get_integer_resource() and similar functions work first.
293 state->display_info = True;
7ce88c8e 294
c428f3d5 295 state->evolution_history = XCreatePixmap(state->dpy, state->win, state->xlim, state->num_generations*state->pixel_size, xgwa.depth);
7ce88c8e
AT
296 // Pixmap contents are undefined after creation. Explicitly set a black
297 // background by drawing a black rectangle over the entire pixmap.
298 XSetForeground(state->dpy, state->gc, state->bg);
c428f3d5 299 XFillRectangle(state->dpy, state->evolution_history, state->gc, 0, 0, state->xlim, state->num_generations*state->pixel_size);
7ce88c8e 300 XSetForeground(state->dpy, state->gc, state->fg);
14d68c5b
AT
301 render_current_generation(state);
302 state->ypos += state->pixel_size;
7ce88c8e
AT
303
304 return state;
0c731d4a
AT
305}
306
0c731d4a
AT
307static unsigned long
308WolframAutomata_draw(Display * dpy, Window win, void * closure)
309{
310// TODO: Mark these basic sections of the function
311//draw()
7ce88c8e
AT
312// calculate (and store) new generation
313// draw new generation as line of pixels on pixmap
314// calculate current 'viewport' into pixmap
315// display on screen
0c731d4a
AT
316// check for termination condition
317
318 struct state * state = closure;
319 int xpos;
7ce88c8e 320 int window_y_offset;
0c731d4a 321
7ce88c8e 322 Bool new_generation[state->xlim];
c428f3d5 323 for (xpos = 0; xpos < state->number_of_cells; xpos++) {
7ce88c8e
AT
324 new_generation[xpos] = calculate_cell(state, xpos);
325 }
c428f3d5 326 for (xpos = 0; xpos < state->number_of_cells; xpos++) {
7ce88c8e
AT
327 state->current_generation[xpos] = new_generation[xpos];
328 }
329 render_current_generation(state);
330
331 // Was this the final generation of this particular simulation? If so, give
332 // the user a moment to bask in the glory of our output and then start a
333 // new simulation.
c428f3d5
AT
334 if (state->ypos/state->pixel_size < state->num_generations-1) {
335 state->ypos += state->pixel_size;
7ce88c8e
AT
336 } else {
337 // TODO: Wait for a second or two, clear the screen and do a new iteration with suitably changed settings.
338 // Note: Since we can't actually loop or sleep here, we need to add a flag to the state struct to indicate that we're in an 'admiration timewindow' (and indicate when it should end)
c428f3d5 339 printf("infinite hamster wheel\n");
7ce88c8e
AT
340 while (1) continue;
341 }
342
343 // Calculate the vertical offset of the current 'window' into the history
344 // of the CA. After the CA's evolution extends past what we can display, have
345 // the window track the current generation and most recent history.
346 if (state->ypos < state->ylim) {
347 window_y_offset = 0;
348 } else {
349 window_y_offset = state->ypos - (state->ylim - 1);
350 }
351
352 // Render everything to the display.
353 XCopyArea(state->dpy, state->evolution_history, state->win, state->gc, 0, window_y_offset, state->xlim, state->ylim, 0, 0);
354 // TODO: Print info on screen if display_info is true. Will need fonts/etc. Do I want to create a separate pixmap for this during the init() function and then just copy the pixmap each time we draw the screen in draw()?
0c731d4a
AT
355
356 return state->delay_microsec;
357}
358
c428f3d5 359// TODO: Fix formatting
0c731d4a
AT
360static const char * WolframAutomata_defaults[] = {
361 ".background: black",
362 ".foreground: white",
80cfe219 363 "*delay-usec: 25000",
7969381e
AT
364 // TODO: Difference between dot and asterisk? Presumably the asterisk matches all resouces of attribute "pixelsize"? Apply answer to all new options.
365 "*pixel-size: 2",
366 "*num-generations: 5000",
80cfe219
AT
367 "*rule-requested: 0",
368 "*rule-random: False",
14d68c5b
AT
369 "*population-density: 50",
370 "*population-single: False",
0c731d4a
AT
371 0
372};
373
c428f3d5 374// TODO: Fix formatting
0c731d4a 375static XrmOptionDescRec WolframAutomata_options[] = {
c428f3d5
AT
376 { "-delay-usec", ".delay-usec", XrmoptionSepArg, 0 },
377 { "-pixel-size", ".pixel-size", XrmoptionSepArg, 0 },
7969381e 378 { "-num-generations", ".num-generations", XrmoptionSepArg, 0 },
80cfe219
AT
379 { "-rule", ".rule-requested", XrmoptionSepArg, 0 },
380 { "-rule-random", ".rule-random", XrmoptionNoArg, "True" },
14d68c5b
AT
381 { "-population-density", ".population-density", XrmoptionSepArg, 0 },
382 { "-population-single", ".population-single", XrmoptionNoArg, "True" },
0c731d4a
AT
383 { 0, 0, 0, 0 }
384};
385
386static Bool
387WolframAutomata_event(Display * dpy, Window win, void * closure, XEvent * event)
388{
389 return False;
390}
391
392static void
393WolframAutomata_free(Display * dpy, Window win, void * closure)
394{
395 struct state * state = closure;
396 XFreeGC(state->dpy, state->gc);
7ce88c8e
AT
397 XFreePixmap(state->dpy, state->evolution_history);
398 free(state->current_generation);
0c731d4a
AT
399 free(state);
400}
401
402static void
403WolframAutomata_reshape(Display * dpy, Window win, void * closure, unsigned int w, unsigned int h)
404{
7ce88c8e 405 WolframAutomata_free(dpy, win, closure);
b0ea929b 406 closure = WolframAutomata_init(dpy, win);
0c731d4a
AT
407}
408
409XSCREENSAVER_MODULE ("1D Nearest-Neighbor Cellular Automata", WolframAutomata)
410