// Hyperbolic Rogue -- fifteen+4 puzzle // Copyright (C) 2011-2019 Zeno Rogue, see 'hyper.cpp' for details /** \file fifteen.cpp * \brief fifteen+4 puzzle */ #include "rogueviz.h" namespace hr { EX namespace fifteen { static const int Empty = 0; struct celldata { int target, targetdir; bool targetmirror; int current, currentdir; bool currentmirror; }; map fif; vector triangle_markers; vector seq; vector turns; eWall empty = waChasm; enum ePenMove { pmJump, pmRotate, pmAdd, pmMirrorFlip }; ePenMove pen; bool show_triangles = false; bool show_dots = true; void init_fifteen(int q = 20) { println(hlog, "init_fifteen"); auto ac = currentmap->allcells(); for(int i=0; icmove(dir); auto mir = where->c.mirror(dir); auto& f0 = fif[where]; auto& f1 = fif[nw]; f0.current = f1.current; f0.currentmirror = f1.currentmirror ^ mir; int d = f1.currentdir; d -= where->c.spin(dir); println(hlog, "mir = ", mir); if(mir) d *= -1; d += dir; if(!mir) d += nw->type/2; f0.currentdir = gmod(d, nw->type); f1.current = Empty; } void check_move() { for(int i=0; itype; i++) { cell *c1 = cwt.at->cmove(i); if(fif.count(c1) && fif[c1].current == Empty) { make_move(c1, cwt.at->c.spin(i)); } } } void scramble() { for(int i=0; i<1000; i++) { int d = hrand(cwt.at->type); cell *c1 = cwt.at->move(d); if(fif.count(c1)) { make_move(cwt.at, d); cwt.at = c1; } } } bool draw_fifteen(cell *c, const shiftmatrix& V) { if(!fif.count(c)) { c->land = laNone; c->wall = waChasm; return false; } check_move(); auto& cd = fif[c]; int cur = anyshiftclick ? cd.target : cd.current; int cdir = anyshiftclick ? cd.targetdir : cd.currentdir; bool cmir = anyshiftclick ? cd.targetmirror : cd.currentmirror; if(cur == Empty) { c->land = laCanvas; c->wall = waNone; c->landparam = 0x101080; } else { c->land = laCanvas; c->wall = waNone; c->landparam = 0xFFFFFF; if(cdir < 0 || cdir >= c->type) { println(hlog, "ERROR: invalid dir ", cdir); cdir = 0; } write_in_space(V * ddspin(c,cdir,0) * (cmir ? MirrorX: Id), 72, 1, dotted(cur), 0xFF, 0, 8); if(show_triangles) { cellwalker cw(c, cdir); cw += triangle_markers[cur] - 1; poly_outline = 0xFF; queuepoly(V * ddspin(c, cw.spin, 0) * xpush(hdist0(tC0(currentmap->adj(c, cw.spin))) * .45 - cgi.zhexf * .3), cgi.shTinyArrow, 0xFF); } } return false; } void edit_fifteen() { if(!fif.count(cwt.at)) init_fifteen(); clearMessages(); getcstat = '-'; cmode = 0; cmode = sm::SIDE | sm::DIALOG_STRICT_X | sm::MAYDARK; auto ss = mapstream::save_start(); ss->item = itGold; gamescreen(0); ss->item = itNone; dialog::init("Fifteen Puzzle", iinf[itPalace].color, 150, 100); dialog::addBoolItem("jump", pen == pmJump, 'j'); dialog::add_action([] { pen = pmJump; }); dialog::addBoolItem("rotate", pen == pmRotate, 'r'); dialog::add_action([] { pen = pmRotate; }); dialog::addBoolItem("mirror flip", pen == pmMirrorFlip, 'f'); dialog::add_action([] { pen = pmMirrorFlip; }); dialog::addBoolItem("add tiles", pen == pmAdd, 'a'); dialog::add_action([] { pen = pmAdd; }); dialog::addBreak(100); dialog::addItem("this is the goal", 'g'); dialog::add_action([] { for(auto& sd: fif) { sd.second.target = sd.second.current; sd.second.targetdir = sd.second.currentdir; sd.second.targetmirror = sd.second.currentmirror; } }); dialog::addItem("remove all tiles", 'r'); dialog::add_action([] { fif.clear(); init_fifteen(1); }); dialog::addItem("scramble", 's'); dialog::add_action(scramble); dialog::addItem("save this puzzle", 'S'); dialog::add_action([] { mapstream::saveMap("fifteen.lev"); #if ISWEB offer_download("fifteen.lev", "mime/type"); #endif }); dialog::addItem("settings", 'X'); dialog::add_action_push(showSettings); mine_adjacency_rule = true; dialog::addItem("new geometry", 'G'); dialog::add_action(runGeometryExperiments); dialog::addItem("load a puzzle", 'L'); dialog::add_action([] { #if ISWEB offer_choose_file([] { mapstream::loadMap("data.txt"); }); #else mapstream::loadMap("fifteen.lev"); #endif mapeditor::drawplayer = false; }); dialog::addBack(); dialog::display(); keyhandler = [] (int sym, int uni) { dialog::handleNavigation(sym, uni); if(sym == '-' && mouseover && !holdmouse) { cell *c = mouseover; if(pen == pmJump) { if(fif.count(c)) { auto& f0 = fif[cwt.at]; auto& f1 = fif[c]; swap(f0, f1); cwt.at = c; } else { fif[c] = fif[cwt.at]; fif.erase(cwt.at); cwt.at = c; } } if(pen == pmRotate) { if(fif.count(c) == 0) return; auto& f1 = fif[c]; f1.currentdir = gmod(1+f1.currentdir, c->type); f1.targetdir = gmod(1+f1.targetdir, c->type); } if(pen == pmMirrorFlip) { if(fif.count(c) == 0) return; auto& f1 = fif[c]; f1.currentmirror ^= true; f1.targetmirror ^= true; } if(pen == pmAdd) { if(fif.count(c) == 0) { auto& f = fif[c]; f.current = f.target = isize(fif)-1; f.currentdir = f.targetdir == 0; } else { auto& f = fif[c]; if(f.current == isize(fif)-1) fif.erase(c); } } compute_triangle_markers(); } else if(doexiton(sym, uni)) popScreen(); }; } void launch() { /* setup */ stop_game(); enable_canvas(); canvas_default_wall = waChasm; start_game(); init_fifteen(); showstartmenu = false; mapeditor::drawplayer = false; } void enable(); void load_fifteen(fhstream& f) { int num; f.read(num); fif.clear(); println(hlog, "read num = ", num); for(int i=0; i(); println(hlog, "at = ", at); cell *c = mapstream::cellbyid[at]; auto& cd = fif[c]; f.read(cd.target); f.read(cd.targetdir); cd.targetdir = mapstream::fixspin(mapstream::relspin[at], cd.targetdir, c->type, f.vernum); if(nonorientable) f.read(cd.targetmirror); f.read(cd.current); f.read(cd.currentdir); if(nonorientable) f.read(cd.currentmirror); cd.currentdir = mapstream::fixspin(mapstream::relspin[at], cd.currentdir, c->type, f.vernum); println(hlog, "assigned ", cd.current, " to ", c); } compute_triangle_markers(); enable(); } void o_key(o_funcs& v) { v.push_back(named_dialog("edit the Fifteen puzzle", edit_fifteen)); } void enable() { rogueviz::rv_hook(hooks_o_key, 80, o_key); rogueviz::rv_hook(hooks_drawcell, 100, draw_fifteen); rogueviz::rv_hook(mapstream::hooks_savemap, 100, [] (fhstream& f) { f.write(15); f.write(isize(fif)); for(auto cd: fif) { f.write(mapstream::cellids[cd.first]); println(hlog, cd.first, " has id ", mapstream::cellids[cd.first]); f.write(cd.second.target); f.write(cd.second.targetdir); if(nonorientable) f.write(cd.second.targetmirror); f.write(cd.second.current); f.write(cd.second.currentdir); if(nonorientable) f.write(cd.second.currentmirror); } }); rogueviz::rv_hook(hooks_clearmemory, 40, [] () { fif.clear(); }); } #if CAP_COMMANDLINE int rugArgs() { using namespace arg; if(0) ; else if(argis("-fifteen")) { PHASEFROM(3); launch(); addHook(mapstream::hooks_loadmap_old, 100, load_fifteen); #if ISWEB mapstream::loadMap("1"); #else enable(); #endif } else if(argis("-fifteen-center")) { // format: -fifteen-center 5 V 0 // to center at 0'th vertex of 5 shift(); int i = argi(); dynamicval ctr(centering); dynamicval lctr(cwt); for(auto& p: fif) { auto& c = p.first; auto& data = p.second; if(data.target == i) { int dir = 0; while(true) { shift(); string s = args(); if(s == "F") { centering = eCentering::face; continue; } if(s == "E") { centering = eCentering::edge; continue; } if(s == "V") { centering = eCentering::vertex; continue; } dir = argi(); break; } cwt = cellwalker(c, dir); fullcenter(); } } playermoved = false; vid.sspeed = -5; } else return 1; return 0; } auto fifteen_hook = addHook(hooks_args, 100, rugArgs) #if CAP_SHOT + arg::add3("-fifteen-animate", [] { rogueviz::rv_hook(anims::hooks_record_anim, 100, [] (int i, int nof) { double at = (i * (isize(seq)-1) * 1.) / nof; int ati = at; double atf = at - ati; hyperpoint h0 = unshift(ggmatrix(seq[ati]) * C0); hyperpoint h1 = unshift(ggmatrix(seq[ati+1]) * C0); atf = atf*atf*(3-2*atf); hyperpoint h2 = lerp(h0, h1, atf); println(hlog, "h0=", h0, " h1=", h1, " h2=", h2); if(invalid_point(h2) || h2[2] < .5) return; h2 = normalize(h2); static ld last_angle = 0; static int last_i = 0; View = gpushxto0(h2) * View; if(ati != last_i && last_angle) { View = spin(-(turns[last_i] - last_angle)) * View; last_angle = 0; } if(true) { ld angle = lerp(0, turns[ati], atf); ld x = -(angle - last_angle); View = spin(x) * View; last_angle = angle; last_i = ati; } anims::moved(); }); }) #endif + addHook(mapstream::hooks_loadmap, 100, [] (fhstream& f, int id) { if(id == 15) load_fifteen(f); }) + addHook(hooks_configfile, 100, [] { param_b(show_dots, "fifteen_dots"); param_b(show_triangles, "fifteen_tris"); }) + addHook_slideshows(120, [] (tour::ss::slideshow_callback cb) { using namespace rogueviz::pres; static vector fifteen_slides; if(fifteen_slides.empty()) { fifteen_slides.emplace_back( slide{"Introduction", 999, LEGAL::NONE, "This is a collection of some geometric and topological variants of the Fifteen puzzle." , [] (presmode mode) {} }); auto add = [&] (string s, string lev, string text, string youtube = "") { fifteen_slides.emplace_back( tour::slide{s, 100, LEGAL::NONE | QUICKGEO, text, [=] (presmode mode) { if(youtube != "") slide_url(mode, 'y', "YouTube link", youtube); string fname = "fifteen/" + lev + ".lev"; if(!file_exists(fname)) { slide_error(mode, "file " + fname + " not found"); return; } setCanvas(mode, '0'); if(mode == pmStart) { slide_backup(mapeditor::drawplayer, mapeditor::drawplayer); slide_backup(vid.wallmode, 2); slide_backup(pconf.scale, .6); stop_game(); mapstream::loadMap(fname); fullcenter(); if(lev == "coiled" || lev == "mobiusband") View = spin(90*degree) * View; if(lev == "mobiusband") View = MirrorX * View; } }}); }; add("15", "classic", "The original Fifteen puzzle."); add("15+4", "fifteen", "The 15+4 puzzle by Henry Segerman.", "https://www.youtube.com/watch?v=Hc3yfuXiWe0"); add("15-4", "sphere11", "The 15-4 puzzle."); add("coiled", "coiled", "Coiled fifteen puzzle by Henry Segerman.", "https://www.youtube.com/watch?v=rfAEgxNEOrQ"); add("Möbius band", "mobiusband", "Fifteen puzzle on a Möbius band."); add("Kite-and-dart", "kitedart", "Kite-and-dart puzzle."); add_end(fifteen_slides); } cb(XLAT("variants of the fifteen puzzle"), &fifteen_slides[0], 'h'); }); #endif EX } EX }