Added CLI flags to WolframAutomata allowing user to specify starting population.
[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 {
85 left_only,
86 middle_only,
87 right_only,
88 random_seed
89};
90
91// TODO: Decorations
92struct curated_ruleset {
93 uint8_t rule;
94 enum seed_population seed;
95};
96
80cfe219 97// TODO: Check the full set of 256 CAs for visually interesting examples.
14d68c5b
AT
98// TODO: Add comments explaining why each ruleset is interesting.
99static const struct curated_ruleset curated_ruleset_list[] = {
100 {110, random_seed}
80cfe219
AT
101};
102
14d68c5b
AT
103/* -------------------------------------------------------------------------- */
104/* Helper Functions */
105/* -------------------------------------------------------------------------- */
106
107// TODO: decorations? inline?
108void
109generate_random_seed(struct state * state)
110{
111 int i;
112 for (i = 0; i < state->number_of_cells; i++) {
113 state->current_generation[i] = ((random() % 100) < state->population_density) ? True : False;
114 }
115}
116
117// TODO: function decorations?
118// TODO: Explain why this santizes the index for accessing current_generation (i.e. it creates a circular topology).
119size_t
120sindex(struct state * state, int index)
121{
122 while (index < 0) {
123 index += state->number_of_cells;
124 }
125 while (index >= state->number_of_cells) {
126 index -= state->number_of_cells;
127 }
128 return (size_t) index;
129}
130
131// TODO: function decorations?
132// TODO: At least give a one-sentence explanation of the algorithm since this function is the core of the simulation.
133Bool
134calculate_cell(struct state * state, int cell_id)
135{
136 uint8_t cell_pattern = 0;
137 int i;
138 for (i = -1; i < 2; i++) {
139 cell_pattern = cell_pattern << 1;
140 if (state->current_generation[sindex(state, cell_id+i)] == True) {
141 cell_pattern |= 1;
142 }
143 }
144 if ((state->rule_number >> cell_pattern) & 1) {
145 return True;
146 } else {
147 return False;
148 }
149}
150
151// TODO: function decorations?
152void
153render_current_generation(struct state * state)
154{
155 size_t xpos;
156 for (xpos = 0; xpos < state->number_of_cells; xpos++) {
157 if (state->current_generation[xpos] == True) {
158 XFillRectangle(state->dpy, state->evolution_history, state->gc, xpos*state->pixel_size, state->ypos, state->pixel_size, state->pixel_size);
159 }
160 }
161}
162
163/* -------------------------------------------------------------------------- */
164/* Screenhack API Functions */
165/* -------------------------------------------------------------------------- */
166
0c731d4a
AT
167static void *
168WolframAutomata_init(Display * dpy, Window win)
169{
7ce88c8e
AT
170 struct state * state = calloc(1, sizeof(*state)); // TODO: Check calloc() call
171 XGCValues gcv;
172 XWindowAttributes xgwa;
14d68c5b 173 const struct curated_ruleset * curated_ruleset = NULL;
7ce88c8e
AT
174
175 state->dpy = dpy;
176 state->win = win;
177
178 XGetWindowAttributes(state->dpy, state->win, &xgwa);
179 state->xlim = xgwa.width;
180 state->ylim = xgwa.height;
181 state->ypos = 0; // TODO: Explain why.
182
183 state->fg = gcv.foreground = get_pixel_resource(state->dpy, xgwa.colormap, "foreground", "Foreground");
184 state->bg = gcv.background = get_pixel_resource(state->dpy, xgwa.colormap, "background", "Background");
185 state->gc = XCreateGC(state->dpy, state->win, GCForeground, &gcv);
186
c428f3d5 187 state->delay_microsec = get_integer_resource(state->dpy, "delay-usec", "Integer");
7ce88c8e
AT
188 if (state->delay_microsec < 0) state->delay_microsec = 0;
189
c428f3d5
AT
190 state->pixel_size = get_integer_resource(state->dpy, "pixel-size", "Integer");
191 if (state->pixel_size < 1) state->pixel_size = 1;
192 if (state->pixel_size > state->xlim) state->pixel_size = state->xlim;
193
194 state->number_of_cells = state->xlim / state->pixel_size;
14d68c5b 195 // TODO: Do we want to enforce that number_of_cells > 0?
c428f3d5 196
80cfe219
AT
197 /* The minimum number of generations is 2 since we must allocate enough */
198 /* space to hold the seed generation and at least one pass through */
199 /* WolframAutomata_draw(), which is where we check whether or not we've */
200 /* reached the end of the pixmap. */
7969381e
AT
201 state->num_generations = get_integer_resource(state->dpy, "num-generations", "Integer");
202 if (state->num_generations < 0) state->num_generations = 2;
203
80cfe219
AT
204 /* Time to figure out which rule to use for this simulation. */
205 /* We ignore any weirdness resulting from the following cast since every */
206 /* bit pattern is also a valid rule; if the user provides weird input, */
207 /* then we'll return weird (but well-defined!) output. */
208 state->rule_requested = (uint8_t) get_integer_resource(state->dpy, "rule-requested", "Integer");
209 state->rule_random = get_boolean_resource(state->dpy, "rule-random", "Boolean");
210 /* Through the following set of branches, we enforce CLI flag precedence. */
211 if (state->rule_random) {
212 /* If this flag is set, the user wants truly random rules rather than */
213 /* random rules from a curated list. */
214 state->rule_number = (uint8_t) random();
215 } else if (state->rule_requested != 0) {
216 /* Rule 0 is terribly uninteresting, so we are reusing it as a 'null' */
217 /* value and hoping nobody notices. Finding a non-zero value means */
218 /* the user requested a specific rule. Use it. */
219 state->rule_number = state->rule_requested;
220 } else {
221 /* No command-line options were specified, so select rules randomly */
222 /* from a curated list. */
14d68c5b
AT
223 size_t number_of_array_elements = sizeof(curated_ruleset_list)/sizeof(curated_ruleset_list[0]);
224 curated_ruleset = &curated_ruleset_list[random() % number_of_array_elements];
225 state->rule_number = curated_ruleset->rule;
226 }
227
228 /* Time to construct the seed generation for this simulation. */
229 state->population_single = get_boolean_resource(state->dpy, "population-single", "Boolean");
230 state->population_density = get_integer_resource(state->dpy, "population-density", "Integer");
231 if (state->population_density < 0 || state->population_density > 100) state->population_density = 50;
232 state->current_generation = calloc(1, sizeof(*state->current_generation)*state->number_of_cells);
233 if (!state->current_generation) {
234 fprintf(stderr, "ERROR: Failed to calloc() in WolframAutomata_init().\n");
235 exit(EXIT_FAILURE);
236 }
237 if (curated_ruleset) {
238 /* If we're using a curated ruleset, ignore any CLI flags related to */
239 /* setting the seed generation, instead drawing that information from */
240 /* the curated ruleset. */
241 switch (curated_ruleset->seed) {
242 case random_seed: generate_random_seed(state); break;
243 case left_only: state->current_generation[0] = True; break;
244 case right_only: state->current_generation[state->number_of_cells-1] = True; break;
245 case middle_only: state->current_generation[state->number_of_cells/2] = True; break;
246 }
247 } else {
248 /* If we're not using a curated ruleset, process any relevant flags */
249 /* from the user, falling back to a random seed generation if nothing */
250 /* else is specified. */
251 if (state->population_single) {
252 state->current_generation[0] = True;
253 } else {
254 generate_random_seed(state);
255 }
80cfe219
AT
256 }
257
7ce88c8e
AT
258 // TODO: These should be command-line options, but I need to learn how the get_integer_resource() and similar functions work first.
259 state->display_info = True;
7ce88c8e 260
c428f3d5 261 state->evolution_history = XCreatePixmap(state->dpy, state->win, state->xlim, state->num_generations*state->pixel_size, xgwa.depth);
7ce88c8e
AT
262 // Pixmap contents are undefined after creation. Explicitly set a black
263 // background by drawing a black rectangle over the entire pixmap.
264 XSetForeground(state->dpy, state->gc, state->bg);
c428f3d5 265 XFillRectangle(state->dpy, state->evolution_history, state->gc, 0, 0, state->xlim, state->num_generations*state->pixel_size);
7ce88c8e 266 XSetForeground(state->dpy, state->gc, state->fg);
14d68c5b
AT
267 render_current_generation(state);
268 state->ypos += state->pixel_size;
7ce88c8e
AT
269
270 return state;
0c731d4a
AT
271}
272
0c731d4a
AT
273static unsigned long
274WolframAutomata_draw(Display * dpy, Window win, void * closure)
275{
276// TODO: Mark these basic sections of the function
277//draw()
7ce88c8e
AT
278// calculate (and store) new generation
279// draw new generation as line of pixels on pixmap
280// calculate current 'viewport' into pixmap
281// display on screen
0c731d4a
AT
282// check for termination condition
283
284 struct state * state = closure;
285 int xpos;
7ce88c8e 286 int window_y_offset;
0c731d4a 287
7ce88c8e 288 Bool new_generation[state->xlim];
c428f3d5 289 for (xpos = 0; xpos < state->number_of_cells; xpos++) {
7ce88c8e
AT
290 new_generation[xpos] = calculate_cell(state, xpos);
291 }
c428f3d5 292 for (xpos = 0; xpos < state->number_of_cells; xpos++) {
7ce88c8e
AT
293 state->current_generation[xpos] = new_generation[xpos];
294 }
295 render_current_generation(state);
296
297 // Was this the final generation of this particular simulation? If so, give
298 // the user a moment to bask in the glory of our output and then start a
299 // new simulation.
c428f3d5
AT
300 if (state->ypos/state->pixel_size < state->num_generations-1) {
301 state->ypos += state->pixel_size;
7ce88c8e
AT
302 } else {
303 // TODO: Wait for a second or two, clear the screen and do a new iteration with suitably changed settings.
304 // 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 305 printf("infinite hamster wheel\n");
7ce88c8e
AT
306 while (1) continue;
307 }
308
309 // Calculate the vertical offset of the current 'window' into the history
310 // of the CA. After the CA's evolution extends past what we can display, have
311 // the window track the current generation and most recent history.
312 if (state->ypos < state->ylim) {
313 window_y_offset = 0;
314 } else {
315 window_y_offset = state->ypos - (state->ylim - 1);
316 }
317
318 // Render everything to the display.
319 XCopyArea(state->dpy, state->evolution_history, state->win, state->gc, 0, window_y_offset, state->xlim, state->ylim, 0, 0);
320 // 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
321
322 return state->delay_microsec;
323}
324
c428f3d5 325// TODO: Fix formatting
0c731d4a
AT
326static const char * WolframAutomata_defaults[] = {
327 ".background: black",
328 ".foreground: white",
80cfe219 329 "*delay-usec: 25000",
7969381e
AT
330 // TODO: Difference between dot and asterisk? Presumably the asterisk matches all resouces of attribute "pixelsize"? Apply answer to all new options.
331 "*pixel-size: 2",
332 "*num-generations: 5000",
80cfe219
AT
333 "*rule-requested: 0",
334 "*rule-random: False",
14d68c5b
AT
335 "*population-density: 50",
336 "*population-single: False",
0c731d4a
AT
337 0
338};
339
c428f3d5 340// TODO: Fix formatting
0c731d4a 341static XrmOptionDescRec WolframAutomata_options[] = {
c428f3d5
AT
342 { "-delay-usec", ".delay-usec", XrmoptionSepArg, 0 },
343 { "-pixel-size", ".pixel-size", XrmoptionSepArg, 0 },
7969381e 344 { "-num-generations", ".num-generations", XrmoptionSepArg, 0 },
80cfe219
AT
345 { "-rule", ".rule-requested", XrmoptionSepArg, 0 },
346 { "-rule-random", ".rule-random", XrmoptionNoArg, "True" },
14d68c5b
AT
347 { "-population-density", ".population-density", XrmoptionSepArg, 0 },
348 { "-population-single", ".population-single", XrmoptionNoArg, "True" },
0c731d4a
AT
349 { 0, 0, 0, 0 }
350};
351
352static Bool
353WolframAutomata_event(Display * dpy, Window win, void * closure, XEvent * event)
354{
355 return False;
356}
357
358static void
359WolframAutomata_free(Display * dpy, Window win, void * closure)
360{
361 struct state * state = closure;
362 XFreeGC(state->dpy, state->gc);
7ce88c8e
AT
363 XFreePixmap(state->dpy, state->evolution_history);
364 free(state->current_generation);
0c731d4a
AT
365 free(state);
366}
367
368static void
369WolframAutomata_reshape(Display * dpy, Window win, void * closure, unsigned int w, unsigned int h)
370{
7ce88c8e 371 WolframAutomata_free(dpy, win, closure);
b0ea929b 372 closure = WolframAutomata_init(dpy, win);
0c731d4a
AT
373}
374
375XSCREENSAVER_MODULE ("1D Nearest-Neighbor Cellular Automata", WolframAutomata)
376