mirror of
				https://github.com/zenorogue/hyperrogue.git
				synced 2025-10-31 14:02:59 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			694 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			694 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| // 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 {
 | |
| 
 | |
| struct puzzle {
 | |
|   string name;
 | |
|   string filename;
 | |
|   string desc;
 | |
|   string url;
 | |
|   string full_filename() const {
 | |
|     return find_file("fifteen/" + filename + ".lev");
 | |
|     }
 | |
|   };
 | |
| 
 | |
| vector<puzzle> puzzles = {
 | |
|   puzzle{"15", "classic", "The original Fifteen puzzle.", ""},
 | |
|   puzzle{"15+4", "fifteen", "The 15+4 puzzle by Henry Segerman.", "https://www.youtube.com/watch?v=Hc3yfuXiWe0"},
 | |
|   puzzle{"15-4", "sphere11", "The 15-4 puzzle.", ""},
 | |
|   puzzle{"coiled", "coiled", "Coiled fifteen puzzle by Henry Segerman.", "https://www.youtube.com/watch?v=rfAEgxNEOrQ"},
 | |
|   puzzle{"Möbius band", "mobiusband", "Fifteen puzzle on a Möbius band.", ""},
 | |
|   puzzle{"Kite-and-dart", "kitedart", "Kite-and-dart puzzle.", ""},
 | |
|   puzzle{"29", "29", "The 29 puzzle by Henry Segerman.", "https://www.youtube.com/watch?v=EitWHthBY30"},
 | |
|   puzzle{"12", "12", "The 12 puzzle mentioned in the same video by Henry Segerman.", "https://www.youtube.com/watch?v=EitWHthBY30"},
 | |
|   puzzle{"124", "124", "The 124 puzzle mentioned in the same video by Henry Segerman.", "https://www.youtube.com/watch?v=EitWHthBY30"},
 | |
|   puzzle{"60", "60", "The 60 puzzle mentioned in the same video by Henry Segerman.", "https://www.youtube.com/watch?v=EitWHthBY30"},
 | |
|   puzzle{"Continental drift", "sphere19", "Based on the Continental Drift puzzle by Henry Segerman.", "https://www.youtube.com/watch?v=0uQx33KFMO0"},
 | |
|   };
 | |
| 
 | |
| puzzle *current_puzzle;
 | |
| reaction_t quitter;
 | |
| 
 | |
| static constexpr int Empty = 0;
 | |
| 
 | |
| struct celldata {
 | |
|   int target, targetdir;
 | |
|   bool targetmirror;
 | |
|   int current, currentdir;
 | |
|   bool currentmirror;
 | |
|   };
 | |
| 
 | |
| map<cell*, celldata> fif;
 | |
| 
 | |
| vector<int> triangle_markers;
 | |
| vector<cell*> seq;
 | |
| vector<ld> turns;
 | |
| 
 | |
| enum class state { edited, unscrambled, scrambled, solved };
 | |
| 
 | |
| state state = state::edited;
 | |
| 
 | |
| 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) {  
 | |
|   auto ac = currentmap->allcells();
 | |
|   for(int i=0; i<min(isize(ac), q); i++) {
 | |
|     fif[ac[i]] = {i, 0, false, i, 0, false};
 | |
|     }  
 | |
|   cwt.at = ac[0];
 | |
|   quitter = [] {};
 | |
|   }
 | |
| 
 | |
| void compute_triangle_markers() {
 | |
|   triangle_markers.resize(isize(fif));
 | |
|   seq.resize(isize(fif));
 | |
|   for(auto& p: fif) {
 | |
|     cell *c = p.first;
 | |
|     
 | |
|     forCellIdEx(c1, i, c) if(fif.count(c1) && fif[c1].target == p.second.target + 1) {
 | |
|       triangle_markers[p.second.target] = gmod((1 + i - p.second.targetdir) * (p.second.targetmirror ? -1 : 1), c->type);
 | |
|       }
 | |
|     
 | |
|     if(p.second.current == 0)
 | |
|       seq.back() = c;
 | |
|     else {
 | |
|       seq[p.second.current-1] = c;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|   println(hlog, triangle_markers);
 | |
| 
 | |
|   for(int i=0; i<isize(fif)-1; i++) {
 | |
|     turns.push_back(triangle_markers[i+1] == 0 ? 90._deg : 0);
 | |
|     }
 | |
|   turns.push_back(0);
 | |
|   }
 | |
| 
 | |
| string dotted(int i) {
 | |
|   string s = its(i);
 | |
|   if(!show_dots) return s;
 | |
|   bool confusing = true;
 | |
|   for(char c: s) if(!among(c, '0', '6', '8', '9') && !(nonorientable && c == '3'))
 | |
|     confusing = false;
 | |
|   if(confusing) s += ".";
 | |
|   return s;
 | |
|   }
 | |
| 
 | |
| /** where = empty square */
 | |
| void make_move(cell *where, int dir) {
 | |
|   auto nw = where->cmove(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);
 | |
|   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; i<cwt.at->type; i++) {
 | |
|     cell *c1 = cwt.at->cmove(i);
 | |
|     if(fif.count(c1) && fif[c1].current == Empty) {
 | |
|       make_move(c1, cwt.at->c.spin(i));
 | |
|       if(state == state::scrambled) {
 | |
|         bool ok = true;
 | |
|         for(auto& [c,f]: fif) {
 | |
|           if(f.current != f.target) ok = false;
 | |
|           if(f.current && (f.currentdir != f.targetdir || f.currentmirror != f.targetmirror))
 | |
|             ok = false;
 | |
|           }
 | |
|         if(ok == true) {
 | |
|           state = state::solved;
 | |
|           #if RVCOL
 | |
|           if(current_puzzle == &puzzles[1]) rogueviz::rv_achievement("FIFTEEN");
 | |
|           #endif
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
| void scramble() {
 | |
|   for(int i=0; i<10000; 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;
 | |
|       }
 | |
|     }     
 | |
|   if(state == state::unscrambled || state == state::solved) state = state::scrambled;
 | |
|   }
 | |
| 
 | |
| bool mouse_over_button;
 | |
| 
 | |
| bool fifteen3 = true;
 | |
| 
 | |
| bool draw_fifteen(cell *c, const shiftmatrix& V) {
 | |
|   hr::addaura(tC0(V), darkened(0x0000FF), 0);
 | |
|   lastexplore = turncount;
 | |
|   if(!fif.count(c)) { c->land = laNone; c->wall = waChasm; c->item = itNone; c->monst = moNone; return false; }
 | |
|   check_move();
 | |
|     
 | |
|   auto& cd = fif[c];
 | |
| 
 | |
|   bool showing = anyshiftclick | mouse_over_button;
 | |
|   
 | |
|   int cur = showing ? cd.target : cd.current;
 | |
|   int cdir = showing ? cd.targetdir : cd.currentdir;
 | |
|   bool cmir = showing ? cd.targetmirror : cd.currentmirror;
 | |
|   
 | |
|   if(cur == Empty) {
 | |
|     c->land = laCanvas;
 | |
|     c->wall = waNone;
 | |
|     c->landparam = 0x101080;
 | |
|     }
 | |
|   else {
 | |
| 
 | |
|     if(fifteen3) {
 | |
|       set_floor(cgi.shFullFloor);
 | |
|       ensure_floorshape_generated(shvid(c), c);
 | |
|       for(int i=0; i<c->type; i++)
 | |
|         if(!fif.count(c->move(i)) || (showing ? fif[c->move(i)].target : fif[c->move(i)].current) == Empty)
 | |
|           placeSidewall(c, i, SIDE::RED1, V, 0xFFFFFFFF);
 | |
|       auto V1 = orthogonal_move_fol(V, cgi.RED[1]);
 | |
|       draw_qfi(c, V1, 0xFFFFFFFF, PPR::WALL_DECO);
 | |
|       write_in_space(V1 * ddspin(c,cdir,0) * (cmir ? MirrorX: Id), 72, 1, dotted(cur), 0xFF, 0, 8);
 | |
|       return true;
 | |
|       }
 | |
| 
 | |
|     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;
 | |
|   }
 | |
| 
 | |
| string fname = "fifteen-saved.lev";
 | |
| void enable();
 | |
| 
 | |
| 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();
 | |
|   ss->item = itNone;
 | |
| 
 | |
|   dialog::init("edit 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("save this puzzle", 'S');
 | |
|   dialog::add_action([] { 
 | |
|     mapeditor::save_level_ext(fname, [] {});
 | |
|     });
 | |
| 
 | |
|   dialog::addItem("load a puzzle", 'L');
 | |
|   dialog::add_action([] { 
 | |
|     auto q = quitter;
 | |
|     mapeditor::load_level_ext(fname, [q] {
 | |
|       for(auto& p: puzzles) if(fname == p.full_filename()) current_puzzle = &p;
 | |
|       quitter = q;
 | |
|       });
 | |
|     });
 | |
| 
 | |
|   dialog::addItem("new geometry", 'G');
 | |
|   dialog::add_action([] {
 | |
|     auto q = quitter;
 | |
|     hook_in_subscreen(hooks_post_initgame, 100, [q] {
 | |
|       init_fifteen(10);
 | |
|       enable();
 | |
|       quitter = q;
 | |
|       state = state::unscrambled;
 | |
|       });
 | |
|     hook_in_subscreen(dialog::hooks_display_dialog, 100, [q] {
 | |
|       for(auto& it: dialog::items)
 | |
|         if(among(it.body, XLAT("land structure"), XLAT("pattern"), XLAT("adjacency rule"))) {
 | |
|           it.type = dialog::diBreak;
 | |
|           it.scale = 0;
 | |
|           dialog::key_actions.erase(it.key);
 | |
|           }
 | |
|       });
 | |
|     runGeometryExperiments();
 | |
|     });
 | |
| 
 | |
|   dialog::addBreak(100);
 | |
|   dialog::addBigItem("options", iinf[itPalace].color);
 | |
| 
 | |
|   dialog::addItem("scramble", 's');
 | |
|   dialog::add_action(scramble);
 | |
| 
 | |
|   dialog::addItem("settings", 'X');
 | |
|   dialog::add_action_push(showSettings);
 | |
| 
 | |
|   mine_adjacency_rule = true;
 | |
| 
 | |
|   if(current_puzzle && current_puzzle->url != "") {
 | |
|     dialog::addItem("Henry Segerman's video", 'V');
 | |
|     dialog::add_action([] {
 | |
|       open_url(current_puzzle->url);
 | |
|       });
 | |
|     }
 | |
| 
 | |
|   add_edit(fifteen3);
 | |
| 
 | |
|   switch(state) {
 | |
|     case state::edited:
 | |
|       dialog::addInfo("edited");
 | |
|       break;
 | |
| 
 | |
|     case state::unscrambled:
 | |
|       dialog::addInfo("not scrambled yet");
 | |
|       break;
 | |
| 
 | |
|     case state::scrambled:
 | |
|       dialog::addInfo("scrambled");
 | |
|       break;
 | |
| 
 | |
|     case state::solved:
 | |
|       dialog::addInfo("solved!");
 | |
|       break;
 | |
|     };
 | |
| 
 | |
|   if(quitter) quitter();
 | |
| 
 | |
|   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)) {
 | |
|           state = state::edited;
 | |
|           auto& f0 = fif[cwt.at];
 | |
|           auto& f1 = fif[c];
 | |
|           swap(f0, f1);
 | |
|           cwt.at = c;
 | |
|           }
 | |
|         else {
 | |
|           state = state::edited;
 | |
|           fif[c] = fif[cwt.at];
 | |
|           fif.erase(cwt.at);
 | |
|           cwt.at = c;
 | |
|           }
 | |
|         }
 | |
|       
 | |
|       if(pen == pmRotate) {
 | |
|         if(fif.count(c) == 0) return;
 | |
|         state = state::edited;
 | |
|         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;
 | |
|         state = state::edited;
 | |
|         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;
 | |
|           state = state::edited;
 | |
|           }
 | |
|         else {
 | |
|           state = state::edited;
 | |
|           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(hstream& f) {
 | |
|   int num;
 | |
|   f.read(num);
 | |
|   fif.clear();
 | |
|   for(int i=0; i<num; i++) {
 | |
|     int32_t at = f.get<int>();
 | |
|     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);
 | |
|     }
 | |
|   compute_triangle_markers();
 | |
|   enable();
 | |
|   state = state::unscrambled;
 | |
|   }
 | |
| 
 | |
| void fifteen_play();
 | |
| 
 | |
| void o_key(o_funcs& v) {
 | |
|   v.push_back(named_functionality("Fifteen interface", [] {
 | |
|      auto s = screens;
 | |
|      pushScreen(fifteen_play);
 | |
|      clearMessages();
 | |
|      quitter = [s] {
 | |
|        dialog::addItem("quit", 'Q');
 | |
|        dialog::add_action([s] { screens = s; });
 | |
|        };
 | |
|      }));
 | |
|   }
 | |
| 
 | |
| 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, [] (hstream& f) {
 | |
|     f.write<int>(15);
 | |
|     f.write<int>(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();
 | |
|     });
 | |
|   rogueviz::rv_hook(hooks_music, 100, [] (eLand& l) { l = mfcode("C2"); return false; });
 | |
|   rogueviz::rv_change(patterns::whichShape, '9');
 | |
|   state = state::edited;
 | |
|   current_puzzle = nullptr;
 | |
|   }
 | |
| 
 | |
| #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<eCentering> ctr(centering);
 | |
|     dynamicval<cellwalker> 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;
 | |
|   }
 | |
| 
 | |
| void default_view_for_puzzle(const puzzle& p) {
 | |
|   auto lev = p.filename;
 | |
|   if(lev == "coiled" || lev == "mobiusband")
 | |
|     View = spin90() * View;
 | |
|   if(lev == "mobiusband")
 | |
|     View = MirrorX * View;
 | |
|   if(hyperbolic) rogueviz::rv_change(pconf.scale, 0.95);
 | |
|   }
 | |
| 
 | |
| void fifteen_play() {
 | |
|   getcstat = '-';
 | |
|   cmode = sm::PANNING;
 | |
|   gamescreen();
 | |
|   stillscreen = true;
 | |
| 
 | |
|   dialog::init();
 | |
| 
 | |
|   clearMessages();
 | |
|   displayButton(vid.fsize, vid.yres - vid.fsize*2, "Shift to see solution", ' ', 0);
 | |
|   displayButton(vid.fsize, vid.yres - vid.fsize, "mouse or WADX to move", ' ', 0);
 | |
|   mouse_over_button = getcstat == ' ';
 | |
| 
 | |
|   displayButton(vid.xres - vid.fsize, vid.yres - vid.fsize, "(v) menu", 'v', 16);
 | |
|   dialog::add_key_action('v', [] { pushScreen(edit_fifteen); });
 | |
| 
 | |
|   keyhandler = [] (int sym, int uni) {
 | |
|     handlePanning(sym, uni);
 | |
|     handle_movement(sym, uni);
 | |
|     if(sym == '-' || sym == PSEUDOKEY_WHEELDOWN) {
 | |
|       actonrelease = false;
 | |
|       mousemovement();
 | |
|       }
 | |
|     dialog::handleNavigation(sym, uni);
 | |
|     };
 | |
|   }
 | |
| 
 | |
| void fifteen_menu() {
 | |
|   cmode = sm::NOSCR;
 | |
|   clearMessages();
 | |
|   gamescreen();
 | |
|   stillscreen = true;
 | |
| 
 | |
|   dialog::init(XLAT("Variants of the Fifteen Puzzle"), 0x2020C0, 200, 0);
 | |
|   char key = 'a';
 | |
|   for(auto& p: puzzles) {
 | |
|     dialog::addBigItem(p.name, key++);
 | |
|     dialog::add_action([&p] {
 | |
|       popScreenAll();
 | |
|       mapstream::loadMap(p.full_filename());
 | |
|       fullcenter();
 | |
|       default_view_for_puzzle(p);
 | |
|       pushScreen([]{ quitmainloop = true; });
 | |
|       pushScreen(fifteen_play);
 | |
|       current_puzzle = &p;
 | |
|       quitter = [] {
 | |
|         dialog::addItem("quit", 'q');
 | |
|         dialog::add_action([] { quitmainloop = true; });
 | |
|         };
 | |
|       });
 | |
|     dialog::addInfo(p.desc);
 | |
|     }
 | |
|   dialog::display();
 | |
|   }
 | |
| 
 | |
| 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, [] (hstream& 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");
 | |
|     param_b(fifteen3, "fifteen_3d")
 | |
|     -> editable("3D Fifteen tile effects", '3');
 | |
|     })
 | |
| + addHook_slideshows(120, [] (tour::ss::slideshow_callback cb) {
 | |
| 
 | |
|     using namespace rogueviz::pres;
 | |
|     static vector<slide> 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. Most of these "
 | |
|           "are digital implementations of the mechanical designs by Henry Segerman."
 | |
|           ,
 | |
|           [] (presmode mode) {}
 | |
|           });
 | |
|       
 | |
|       for(auto p: puzzles) {
 | |
|         fifteen_slides.emplace_back(
 | |
|           tour::slide{p.name, 100, LEGAL::NONE | QUICKGEO, p.desc,
 | |
|             [=] (presmode mode) {
 | |
|               if(p.url != "")
 | |
|                 slide_url(mode, 'y', "YouTube link", p.url);
 | |
|               string fname = p.full_filename();
 | |
|               if(!file_exists(fname)) {
 | |
|                 slide_error(mode, "file " + fname + " not found");
 | |
|                 return;
 | |
|                 }
 | |
|               setWhiteCanvas(mode);
 | |
|               if(mode == pmStart) {
 | |
|                 slide_backup(mapeditor::drawplayer, mapeditor::drawplayer);
 | |
|                 slide_backup(vid.wallmode, 2);
 | |
|                 slide_backup(pconf.scale, .6);
 | |
|                 slide_backup(no_find_player, true);
 | |
|                 stop_game();
 | |
|                 mapstream::loadMap(fname);
 | |
|                 popScreenAll();
 | |
|                 fullcenter();
 | |
|                 default_view_for_puzzle(p);
 | |
|                 }
 | |
|               }});
 | |
|         };
 | |
| 
 | |
|       add_end(fifteen_slides);
 | |
|       }
 | |
| 
 | |
|     cb(XLAT("variants of the fifteen puzzle"), &fifteen_slides[0], 'f');
 | |
|     });
 | |
| #endif
 | |
| 
 | |
| EX }
 | |
| 
 | |
| EX }
 | |
| 
 | 
