1 #include "rogueviz.h"
2 
3 // SAG visualizer (e.g. Reddit roguelikes, GitHub languages)
4 //-----------------------------------------------------------
5 
6 // see: https://www.youtube.com/watch?v=mDG3_f8R2Ns (SAG boardgames)
7 // see: https://www.youtube.com/watch?v=WSyygk_3j9o (SAG roguelikes)
8 // see: https://www.youtube.com/watch?v=HWQkDkeEUeM (SAG programming languages)
9 
10 namespace rogueviz {
11 
12 namespace sag {
13 
14   bool turn(int delta);
15 
16   int sagpar = 0;
17 
18   enum eSagmode { sagOff, sagHC, sagSA };
19 
20   eSagmode sagmode; // 0 - off, 1 - hillclimbing, 2 - SA
21 
22   const char *sagmodes[3] = {"off", "HC", "SA"};
23 
24   ld temperature = -4;
25   const int INSNAKE = 117;
26   int numsnake;
27   const char *loadfname;
28 
29   #define MAXSNAKETAB 1000
30   int sdist[MAXSNAKETAB][MAXSNAKETAB];
31   int insnaketab = 0;
32 
33   vector<cell*> snakecells;
34   vector<int> snakefirst, snakelast;
35   vector<int> snakenode;
36   vector<int> snakeid;
37   vector<int> lpbak;
38   vector<int> wpbak;
39 
40   bool snake_enabled;
41 
setsnake(cellwalker & cw,int i)42   void setsnake(cellwalker& cw, int i) {
43     lpbak[i] = cw.at->landparam;
44     wpbak[i] = cw.at->wparam;
45     cw.at->landparam = i; cw.at->wparam = INSNAKE;
46     // cw.at->monst = moWormtail; cw.at->mondir = cw.spin;
47     snakecells[i] = cw.at;
48     }
49 
snakeswitch()50   void snakeswitch() {
51     for(int i=0; i<numsnake; i++) {
52       cell *c = snakecells[i];
53       int x;
54       x = lpbak[i]; lpbak[i] = c->landparam; c->landparam = x;
55       x = wpbak[i]; wpbak[i] = c->wparam; c->wparam = x;
56       }
57     snake_enabled = !snake_enabled;
58     }
59 
enable_snake()60   void enable_snake() { if(!snake_enabled) snakeswitch(); }
61 
disable_snake()62   void disable_snake() { if(snake_enabled) snakeswitch(); }
63 
snakedist(int i,int j)64   int snakedist(int i, int j) {
65     if(i < insnaketab && j < insnaketab) return sdist[i][j];
66     if(bounded) return celldistance(snakecells[i], snakecells[j]);
67     int i0 = i, i1 = i, j0 = j, j1 = j;
68     int cost = 0;
69     // intersect
70     while(true) {
71       if(j0 > i1+1) { j0 = snakefirst[j0], j1 = snakelast[j1]; cost++; }
72       else if(i0 > j1+1) { i0 = snakefirst[i0], i1 = snakelast[i1]; cost++; }
73       else if(j1+1 == i0) return cost+1;
74       else if(i1+1 == j0) return cost+1;
75       else return cost;
76       }
77     }
78 
initSnake(int n)79   void initSnake(int n) {
80     if(bounded) n = isize(currentmap->allcells());
81     numsnake = n;
82     snakecells.resize(numsnake);
83     snakefirst.resize(numsnake);
84     snakelast.resize(numsnake);
85     snakenode.resize(numsnake);
86     lpbak.resize(numsnake);
87     wpbak.resize(numsnake);
88     if(bounded) {
89       for(int i=0; i<n; i++) {
90         cellwalker cw(currentmap->allcells()[i], 0);
91         setsnake(cw, i);
92         }
93       }
94     else {
95       cellwalker cw = cwt;
96       setsnake(cw, 0);
97       cw += wstep;
98       setsnake(cw, 1);
99       for(int i=2; i<=numsnake; i++) {
100         if(i == numsnake && sphere) break;
101         cw += wstep;
102         snakefirst[i-1] = cw.at->landparam;
103         while(cw.at->wparam == INSNAKE) {
104           snakelast[i-1] = cw.at->landparam;
105           cw = cw + wstep + 1 + wstep;
106           }
107         if(i == numsnake) break;
108         setsnake(cw, i); cw += 1;
109         }
110       }
111     int stab = min(numsnake, MAXSNAKETAB);
112     for(int i=0; i<stab; i++)
113     for(int j=0; j<stab; j++)
114       sdist[i][j] = snakedist(i,j);
115     insnaketab = stab;
116     snake_enabled = true;
117     }
118 
119   ld hub_penalty;
120   string hub_filename;
121   vector<int> hubval;
122 
costat(int vid,int sid)123   double costat(int vid, int sid) {
124     if(vid < 0) return 0;
125     double cost = 0;
126     vertexdata& vd = vdata[vid];
127     for(int j=0; j<isize(vd.edges); j++) {
128       edgeinfo *ei = vd.edges[j].second;
129       int t2 = vd.edges[j].first;
130       if(snakeid[t2] != -1) cost += snakedist(sid, snakeid[t2]) * ei->weight2;
131       }
132 
133     if(!hubval.empty()) {
134       cell *c = snakecells[sid];
135       for(int i=0; i<c->type; i++) {
136         cell *c2 = c->move(i);
137         if(c2 && c2->wparam == INSNAKE) {
138           int vid2 = snakenode[c2->landparam];
139           if(vid2 >= 0 && (hubval[vid] & hubval[snakenode[c2->landparam]]) == 0)
140             cost += hub_penalty;
141           }
142         }
143       }
144 
145     /* cell *c = snakecells[id];
146     for(int i=0; i<c->type; i++) {
147       cell *c2 = c->move(i);
148       if(c2 && c2->wparam == INSNAKE && snakenode[c2->landparam] >= 0)
149         cost += 100;
150       } */
151     return cost;
152     }
153 
154   // std::mt19937 los;
155 
156   bool infullsa;
157 
158   double cost;
159   int N;
160 
161   vector<double> chgs;
162 
163   edgetype *sag_edge;
164 
forgetedges(int id)165   void forgetedges(int id) {
166     for(int i=0; i<isize(vdata[id].edges); i++)
167       vdata[id].edges[i].second->orig = NULL;
168     }
169 
chance(double p)170   bool chance(double p) {
171     p *= double(hrngen.max()) + 1;
172     auto l = hrngen();
173     auto pv = (decltype(l)) p;
174     if(l < pv) return true;
175     if(l == pv) return chance(p-pv);
176     return false;
177     }
178 
saiter()179   void saiter() {
180     aiter:
181 
182     int t1 = hrand(N);
183     int sid1 = snakeid[t1];
184 
185     int sid2;
186 
187     int s = hrand(6);
188 
189     if(s == 3) s = 2;
190     if(s == 4) s = 5;
191 
192     if((sagpar&1) && (s == 2 || s == 3 || s == 4)) return;
193 
194     if(s == 5) sid2 = hrand(numsnake);
195 
196     else {
197       cell *c;
198       if(s>=2 && isize(vdata[t1].edges)) c = snakecells[snakeid[hrand(isize(vdata[t1].edges))]];
199       else c = snakecells[sid1];
200 
201       int it = s<2 ? (s+1) : s-2;
202       for(int ii=0; ii<it; ii++) {
203         int d = hrand(c->type);
204         c = c->move(d);
205         if(!c) goto aiter;
206         if(c->wparam != INSNAKE) goto aiter;
207         }
208       sid2 = c->landparam;
209       }
210     int t2 = snakenode[sid2];
211 
212     snakenode[sid1] = -1; snakeid[t1] = -1;
213     snakenode[sid2] = -1; if(t2 >= 0) snakeid[t2] = -1;
214 
215     double change =
216       costat(t1,sid2) + costat(t2,sid1) - costat(t1,sid1) - costat(t2,sid2);
217 
218     snakenode[sid1] = t1; snakeid[t1] = sid1;
219     snakenode[sid2] = t2; if(t2 >= 0) snakeid[t2] = sid2;
220 
221     if(change < 0) chgs.push_back(-change);
222 
223     if(change > 0 && (sagmode == sagHC || !chance(exp(-change * exp(-temperature))))) return;
224 
225     snakenode[sid1] = t2; snakenode[sid2] = t1;
226     snakeid[t1] = sid2; if(t2 >= 0) snakeid[t2] = sid1;
227     if(vdata[t1].m) vdata[t1].m->base = snakecells[sid2];
228     if(t2 >= 0 && vdata[t2].m) vdata[t2].m->base = snakecells[sid1];
229     cost += 2*change;
230 
231     if(t1 >= 0) forgetedges(t1);
232     if(t2 >= 0) forgetedges(t2);
233     }
234 
organize()235   void organize() {
236     for(int i=0; i<numsnake; i++) snakenode[i] = -1;
237     vector<int> freenodes;
238     for(int i=0; i<N; i++)
239       if(snakeid[i] != -1)
240         snakenode[snakeid[i]] = i;
241 
242     for(int i=0; i<N; i++)
243       if(snakeid[i] != -1)
244         if(snakenode[snakeid[i]] != i)
245           snakeid[i] = -1;
246 
247     for(int i=0; i<numsnake; i++)
248       if(snakenode[i] == -1)
249         freenodes.push_back(i);
250 
251     int j = 0;
252     for(int i=0; i<N; i++)
253       if(snakeid[i] == -1) {
254         snakeid[i] = freenodes[j];
255         snakenode[freenodes[j]] = i;
256         j++;
257         }
258     cost = 0; for(int i=0; i<N; i++) cost += costat(i, i);
259     }
260 
loadsnake(const string & fname)261   void loadsnake(const string& fname) {
262     printf("Loading the sag from: %s\n", fname.c_str());
263     FILE *sf = fopen(fname.c_str(), "rt");
264     if(!sf) { printf("Failed to open file.\n"); exit(1); }
265     if(sf) while(true) {
266       string lab;
267       while(true) {
268         int c = fgetc(sf);
269         if(c == EOF) goto afterload;
270         else if(c == ',' || c == ';') break;
271         else if(rv_ignore(c)) ;
272         else lab += c;
273         }
274       int sid = -1;
275       int err = fscanf(sf, "%d", &sid);
276       if(sid < 0 || sid >= numsnake || err < 1) sid = -1;
277       if(!labeler.count(lab)) {
278         printf("unknown vertex: %s\n", lab.c_str());
279         }
280       else {
281         int id = getid(lab);
282         snakeid[id] = sid;
283         }
284       }
285     afterload:
286     if(sf) fclose(sf);
287 
288     organize();
289     for(int i=0; i<N; i++) {
290       if(vdata[i].m) vdata[i].m->base = snakecells[sag::snakeid[i]];
291       forgetedges(i);
292       }
293 
294     shmup::fixStorage();
295     }
296 
297   vector<edgeinfo> sagedges;
298 
299   /* bool totcmp(int i, int j) {
300     return totwei[i] > totwei[j];
301     } */
302 
303   int ipturn = 100;
304   int numiter = 0;
305 
306   int hightemp = 10;
307   int lowtemp = -15;
308 
dofullsa(int satime)309   void dofullsa(int satime) {
310     sagmode = sagSA;
311     enable_snake();
312     int t1 = SDL_GetTicks();
313 
314     while(true) {
315       int t2 = SDL_GetTicks();
316       double d = (t2-t1) / (1000. * satime);
317       if(d > 1) break;
318       temperature = hightemp - (d*(hightemp-lowtemp));
319       chgs.clear();
320       for(int i=0; i<50000; i++) {
321         numiter++;
322         sag::saiter();
323         }
324       DEBB(DF_LOG, (format("it %8d temp %6.4f [1/e at %13.6f] cost = %f ",
325         numiter, double(sag::temperature), (double) exp(sag::temperature),
326         double(sag::cost))));
327 
328       sort(chgs.begin(), chgs.end());
329       int cc = chgs.size() - 1;
330       DEBB(DF_LOG, (format("%9.4f .. %9.4f .. %9.4f .. %9.4f .. %9.4f\n",
331         double(chgs[0]), double(chgs[cc/4]), double(chgs[cc/2]), double(chgs[cc*3/4]), double(chgs[cc]))));
332       fflush(stdout);
333       }
334 
335     temperature = -5;
336     disable_snake();
337     sagmode = sagOff;
338     }
339 
iterate()340   void iterate() {
341     if(!sagmode) return;
342     int t1 = SDL_GetTicks();
343     enable_snake();
344     for(int i=0; i<ipturn; i++) {
345       numiter++;
346       sag::saiter();
347       }
348     disable_snake();
349     int t2 = SDL_GetTicks();
350     int t = t2 - t1;
351     if(t < 50) ipturn *= 2;
352     else if(t > 200) ipturn /= 2;
353     else ipturn = ipturn * 100 / t;
354     DEBB(DF_LOG, ("it %8d temp %6.4f [2:%8.6f,10:%8.6f,50:%8.6f] cost = %f\n",
355       numiter, double(sag::temperature),
356       (double) exp(-2 * exp(-sag::temperature)),
357       (double) exp(-10 * exp(-sag::temperature)),
358       (double) exp(-50 * exp(-sag::temperature)),
359       (double) sag::cost));
360     }
361 
savesnake(const string & fname)362   void savesnake(const string& fname) {
363     FILE *f = fopen(fname.c_str(), "wt");
364     for(int i=0; i<N; i++)
365       fprintf(f, "%s;%d\n", vdata[i].name.c_str(), snakeid[i]);
366     fclose(f);
367     }
368 
loglik()369   void loglik() {
370     int indist[30], pedge[30];
371     for(int d=0; d<30; d++) indist[d] = 0, pedge[d] = 0;
372 
373     for(int i=0; i<N; i++)
374     for(int j=0; j<i; j++)
375       indist[snakedist(snakeid[i], snakeid[j])]++;
376 
377     for(int i=0; i<isize(sagedges); i++) {
378       edgeinfo& ei = sagedges[i];
379       if(snakedist(snakeid[ei.i], snakeid[ei.j]) == 0) {
380         printf("zero between %d (%s) and %d (%s)\n",
381           snakeid[ei.i], vdata[ei.i].name.c_str(),
382           snakeid[ei.j], vdata[ei.j].name.c_str());
383         }
384       if(ei.weight >= sag_edge->visible_from)
385         pedge[snakedist(snakeid[ei.i], snakeid[ei.j])]++;
386       }
387 
388     for(int d=0; d<30; d++)
389       if(indist[d])
390         printf("%2d: %7d/%7d %7.3lf\n",
391           d, pedge[d], indist[d], double(pedge[d] * 100. / indist[d]));
392 
393     ld loglik = 0;
394     for(int d=0; d<30; d++) {
395       int p = pedge[d], pq = indist[d];
396       int q = pq - p;
397       if(p && q)
398         loglik += p * log(p) + q * log(q) - pq * log(pq);
399       }
400 
401     println(hlog, "loglikelihood = ", fts(loglik));
402     }
403 
read_hubs(const string & fname)404   void read_hubs(const string& fname) {
405     hubval.resize(isize(vdata), -1);
406     fhstream f(fname, "rt");
407     if(!f.f) { printf("Failed to open hub file: %s\n", fname.c_str()); exit(1); }
408     println(hlog, "loading hubs: ", fname);
409     while(!feof(f.f)) {
410       string l1, l2;
411       while(true) {
412         int c = fgetc(f.f);
413         if(c == EOF) return;
414         else if(c == ';') break;
415         else if(rv_ignore(c)) ;
416         else l1 += c;
417         }
418       while(true) {
419         int c = fgetc(f.f);
420         if(c == EOF) return;
421         else if(c == ';') return;
422         else if(rv_ignore(c)) break;
423         else l2 += c;
424         }
425       if(!id_known(l1)) {
426         printf("label unknown: %s\n", l1.c_str());
427         exit(1);
428         }
429       hubval[getid(l1)] = atoi(l2.c_str());
430       }
431     }
432 
readsag(const char * fname)433   void readsag(const char *fname) {
434     maxweight = 0;
435     sag_edge = add_edgetype("SAG edge");
436     fhstream f(fname, "rt");
437     if(!f.f) { printf("Failed to open SAG file: %s\n", fname); exit(1); }
438     // while(fgetc(f) != 10 && fgetc(f) != 13 && !feof(f)) ;
439     while(!feof(f.f)) {
440       string l1, l2;
441       while(true) {
442         int c = fgetc(f.f);
443         if(c == EOF) return;
444         else if(c == ';') break;
445         else if(rv_ignore(c)) ;
446         else l1 += c;
447         }
448       while(true) {
449         int c = fgetc(f.f);
450         if(c == EOF) return;
451         else if(c == ';') break;
452         else if(rv_ignore(c)) ;
453         else l2 += c;
454         }
455       ld wei;
456       if(!scan(f, wei)) continue;
457       edgeinfo ei(sag_edge);
458       ei.i = getid(l1);
459       ei.j = getid(l2);
460       ei.weight = wei;
461       sagedges.push_back(ei);
462       }
463     }
464 
465   ld edgepower=1, edgemul=1;
466 
read(string fn)467   void read(string fn) {
468     fname = fn;
469     init(RV_GRAPH | RV_WHICHWEIGHT | RV_AUTO_MAXWEIGHT | RV_HAVE_WEIGHT);
470 
471     rv_hook(rogueviz::hooks_close, 100, [] { sag::sagedges.clear(); });
472     rv_hook(shmup::hooks_turn, 100, turn);
473     rv_hook(rogueviz::hooks_rvmenu, 100, [] {
474       dialog::addSelItem(XLAT("temperature"), fts(sag::temperature), 't');
475       dialog::add_action([] {
476         dialog::editNumber(sag::temperature, sag::lowtemp, sag::hightemp, 1, 0, XLAT("temperature"), "");
477         });
478       dialog::addSelItem(XLAT("SAG mode"), sag::sagmodes[sag::sagmode], 'm');
479       dialog::add_action([] { sag::sagmode = sag::eSagmode( (1+sag::sagmode) % 3 ); });
480       });
481 
482     weight_label = "min weight";
483     temperature = 0; sagmode = sagOff;
484     readsag(fname.c_str());
485     if(hub_filename != "")
486       read_hubs(hub_filename);
487 
488     N = isize(vdata);
489     // totwei.resize(N);
490     // for(int i=0; i<N; i++) totwei[i] = 0;
491 
492     for(int i=0; i<N; i++) vdata[i].data = 0;
493     /* for(int i=0; i<isize(sagedges); i++) {
494       edgeinfo& ei = sagedges[i];
495       // maxwei[ei.i] = max(maxwei[ei.i], ei.weight);
496       // maxwei[ei.j] = max(maxwei[ei.j], ei.weight);
497       // totwei[ei.i] += ei.weight;
498       // totwei[ei.j] += ei.weight;
499       } */
500     for(int i=0; i<isize(sagedges); i++) {
501       edgeinfo& ei = sagedges[i];
502       // (ei.weight >= maxwei[ei.i] / 5 || ei.weight >= maxwei[ei.j] / 5);
503 
504       ei.weight2 = pow((double) ei.weight, (double) edgepower) * edgemul;
505       // LANG:: pow(ei.weight, .4) / 50;
506 
507       // ei.weight2 = 0; int w = ei.weight; while(w) { w >>= 1; ei.weight2++; }
508       /* if(totwei[ei.i] <= 0 || totwei[ei.j] <= 0) {
509         printf("BAD TOTWEI\n");
510         exit(1);
511         }
512       ei.weight2 = 3 * (
513         sqrt(ei.weight * 1. / totwei[ei.i]) * log(totwei[ei.i]) * log(totwei[ei.i]) +
514         sqrt(ei.weight * 1. / totwei[ei.j]) * log(totwei[ei.j]) * log(totwei[ei.j])); */
515       // printf("%f\n", ei.weight2);
516       addedge0(ei.i, ei.j, &ei);
517       }
518 
519     initSnake(N*2);
520     printf("numsnake = %d\n", numsnake);
521     if(numsnake < N) {
522       printf("Error: snake does not fit\n");
523       exit(1);
524       }
525     snakeid.resize(N);
526     for(int i=0; i<N; i++) snakeid[i] = -1;
527     organize();
528     disable_snake();
529 
530     for(int i=0; i<N; i++) {
531       int ii = i;
532       vertexdata& vd = vdata[ii];
533       vd.cp = colorpair(dftcolor);
534       createViz(ii, sag::snakecells[sag::snakeid[i]], Id);
535       }
536 
537     storeall();
538     }
539 
readArgs()540 int readArgs() {
541 #if CAP_COMMANDLINE
542   using namespace arg;
543 
544   if(0) ;
545 
546   else if(argis("-sagmin")) {
547     shift_arg_formula(default_edgetype.visible_from);
548     default_edgetype.visible_from_hi = default_edgetype.visible_from;
549     default_edgetype.visible_from_help = default_edgetype.visible_from;
550     }
551   else if(argis("-sagminhi")) {
552     shift_arg_formula(default_edgetype.visible_from_hi);
553     }
554   else if(argis("-sagminhelp")) {
555     shift_arg_formula(default_edgetype.visible_from_help);
556     }
557 
558 // (1) configure edge weights
559   else if(argis("-edgepower")) {
560     shift_arg_formula(sag::edgepower);
561     shift_arg_formula(sag::edgemul);
562     }
563 // (1) configure temperature (high, low)
564   else if(argis("-sagtemp")) {
565     shift(); sag::hightemp = argi();
566     shift(); sag::lowtemp = argi();
567     }
568 // (2) read the edge data
569   else if(argis("-sagpar")) {
570     PHASE(3);
571     shift();
572     sag::sagpar = argi();
573     }
574   else if(argis("-sag")) {
575     PHASE(3);
576     shift(); sag::read(args());
577     }
578   else if(argis("-saghubs")) {
579     println(hlog, "HUBS");
580     PHASE(3);
581     shift_arg_formula(sag::hub_penalty);
582     shift(); hub_filename = args();
583     }
584 // (3) load the initial positioning
585   else if(argis("-gload")) {
586     PHASE(3); shift(); sag::loadsnake(args());
587     }
588 // (4) perform simulated annealing: -fullsa <time in seconds>
589   else if(argis("-fullsa")) {
590     shift(); sag::dofullsa(argi());
591     }
592 // (5) save the positioning
593   else if(argis("-gsave")) {
594     PHASE(3); shift(); sag::savesnake(args());
595     }
596 // (6) output loglikelihood
597   else if(argis("-lik")) {
598     sag::loglik();
599     }
600   else return 1;
601 #endif
602   return 0;
603   }
604 
turn(int delta)605 bool turn(int delta) {
606   return false;
607   // shmup::pc[0]->rebase();
608   }
609 
cname()610 string cname() {
611   if(euclid) return "coord-6.txt";
612   if(PURE) return "coord-7.txt";
613   return "coord-67.txt";
614   }
615 
616 int ah = addHook(hooks_args, 100, readArgs)
__anon888a3ae20502(string s, vector<tour::slide>& v) 617   + addHook_rvslides(120, [] (string s, vector<tour::slide>& v) {
618     if(s != "data") return;
619     using namespace pres;
620     string sagf = "SAG/";
621     v.push_back(
622       slide{sagf+"Roguelikes", 63, LEGAL::UNLIMITED | QUICKGEO,
623         "A visualization of roguelikes, based on discussion on /r/reddit. "
624         "See: http://www.roguetemple.com/z/hyper/reddit.php",
625         roguevizslide('0', [] () {
626           rogueviz::dftcolor = 0x282828FF;
627 
628           rogueviz::showlabels = true;
629           part(rogueviz::default_edgetype.color, 0) = 181;
630           rogueviz::sag::edgepower = 1;
631           rogueviz::sag::edgemul = 1;
632 
633           gmatrix.clear();
634           drawthemap();
635           gmatrix0 = gmatrix;
636 
637           rogueviz::sag::read(RVPATH "roguelikes/edges.csv");
638           rogueviz::readcolor(RVPATH "roguelikes/color.csv");
639           rogueviz::sag::loadsnake(RVPATH "roguelikes/" + cname());
640           })
641         }
642       );
643     v.push_back(slide  {sagf+"Programming languages of GitHub", 64, LEGAL::UNLIMITED | QUICKGEO,
644     "A visualization of programming languages.",
645     roguevizslide('0', [] () {
646       rogueviz::dftcolor = 0x282828FF;
647 
648       rogueviz::showlabels = true;
649       part(rogueviz::default_edgetype.color, 0) = 128;
650       rogueviz::sag::edgepower = .4;
651       rogueviz::sag::edgemul = .02;
652 
653       gmatrix.clear();
654       drawthemap();
655       gmatrix0 = gmatrix;
656 
657       rogueviz::sag::read(RVPATH "lang/edges.csv");
658       rogueviz::readcolor(RVPATH "lang/color.csv");
659       rogueviz::sag::loadsnake(RVPATH "lang/" + cname());
660       if(euclid) rogueviz::legend.clear();
661       })
662     });
663 
664     v.push_back(slide {sagf+"Boardgames", 62, LEGAL::UNLIMITED | QUICKGEO,
665         "A visualization of board games, based on discussions on Reddit.",
666     roguevizslide('0', [] () {
667       rogueviz::dftcolor = 0x282828FF;
668 
669       rogueviz::showlabels = true;
670       part(rogueviz::default_edgetype.color, 0) = 157;
671       rogueviz::sag::edgepower = 1;
672       rogueviz::sag::edgemul = 1;
673 
674       gmatrix.clear();
675       drawthemap();
676       gmatrix0 = gmatrix;
677 
678       rogueviz::sag::read(RVPATH "boardgames/edges.csv");
679       rogueviz::readcolor(RVPATH "boardgames/color.csv");
680       rogueviz::sag::loadsnake(RVPATH "boardgames/" + cname());
681       })
682         });
683 
684     });
685 
686 EX }
687 
688 }
689