// non-Euclidean falling block game, implemented using the HyperRogue engine // Copyright (C) 2011-2021 Zeno Rogue, see 'hyper.cpp' for details #define BRINGRIS_VER "2.0" #ifdef BRINGRIS #define CUSTOM_CAPTION "Bringris " BRINGRIS_VER #define MAXMDIM 4 #define CAP_INV 0 #define CAP_COMPLEX2 0 #define CAP_EDIT 0 #ifdef BWEB #define CAP_BT 0 #define CAP_SOLV 0 #endif #define CAP_THREAD 0 // #define CAP_ZLIB 0 #define CAP_FILES 1 #define CAP_CONFIG 1 // #define CAP_ANIMATIONS 0 #define CAP_RUG 0 #define CAP_SHOT 0 #define CAP_SVG 0 #define CAP_PNG 0 #define CAP_WRL 0 #define CAP_TOUR 0 #define CAP_IRR 0 #define CAP_CRYSTAL 0 #define CAP_ARCM 0 #define CAP_HISTORY 0 #define CAP_STARTANIM 0 #define CAP_SAVE 0 #define CAP_TRANS 0 #ifdef BWEB #include "../hyperweb.cpp" #else #include "../hyper.cpp" #endif #include "subquotient.cpp" #define SUBQ #endif #include "../hyper.h" #ifdef RVCOL namespace hr { void rv_achievement(const string& name); void rv_leaderboard(const string& name, int score); } #endif #define solnil (nil || sol) namespace hr { namespace subquotient { #ifndef SUBQ void create_subquotient(int qty = -1, int id = 0); #endif extern eGeometry gSubquotient; } namespace bringris { local_parameter_set lps_bringris("bringris:"); local_parameter_set lps_bringris_explore("bringris:explore:", &lps_bringris); local_parameter_set lps_bringris_play("bringris:play:", &lps_bringris); multi::config scfg_bringris; struct bgeometry { string name; string cap; flagtype flags; reaction_t create; }; enum eBringrisMove { bmDown, bmLeft, bmUp, bmRight, bmTurnLeft, bmTurnRight, bmDrop, bmFullDrop, bmPause, bmNothing, bmLast }; vector move_names = { "move down", "move left", "move up", "move right", "turn left", "turn right", "drop by one", "full drop", "pause", "do nothing" }; int camera_level; ld ang = 0, cur_ang = 0; int lti; int bgeom = 0; int max_piece; int default_max_piece; bool rotate_allowed = false; bool in_bringris; bool use_raycaster = true; bool flashes = true; int last_adjust, when_t; transmatrix tView, pView; cell* ncenter; cell *well_center; vector level; vector out_level; map center_distance; bool pro_game; int well_size = 10; int camera = 3; int facing_mod = 0; int draw_per_level = 2000; int shape_id, next_shape_id; cellwalker at; int move_started; int move_at; int completed; int bricks, cubes; ld score; bool paused; bool explore; enum eState { tsPreGame, tsFalling, tsBetween, tsCollect, tsGameover }; eState state = tsPreGame; constexpr flagtype HYPERBOLIC = 1; constexpr flagtype ORBIFOLD = 2; constexpr flagtype SECRET = 4; constexpr flagtype NONORIENTABLE = 8; constexpr flagtype SPHERICAL = 16; constexpr flagtype EUCLIDEAN = 32; constexpr flagtype SUBQUOTIENT = 64; constexpr flagtype HDUAL = 128; constexpr flagtype BOUNDED_WELL = 256; constexpr flagtype ASYMMETRIC_ONLY = 512; constexpr flagtype FLAT_ONLY = 1024; cell *get_center(); cell *shift_block_target(int dir); void shift_block(int dir, bool camera_only = false); void rotate_block(int dir, bool camera_only = false); void start_new_game(); void save(); void load(); vector bgeoms = { {"Bring surface", "the original Bringris geometry", HYPERBOLIC, [] { using namespace fieldpattern; current_extra = 2; auto& gxcur = fgeomextras[current_extra]; while(isize(gxcur.primes) < 1) nextPrime(gxcur); fgeomextras[current_extra].current_prime_id = 0; enableFieldChange(); set_geometry(gFieldQuotient); gp::param = gp::loc(1, 1); set_variation(eVariation::unrectified); set_geometry(gProduct); default_max_piece = 4; rotate_allowed = false; }}, {"torus", "Euclidean level geometry", EUCLIDEAN, [] { auto& T0 = euc::eu_input.user_axes; T0[0][0] = 5; T0[0][1] = 0; T0[1][0] = 0; T0[1][1] = 5; euc::eu_input.twisted = 0; set_geometry(gEuclidSquare); set_variation(eVariation::pure); set_geometry(gProduct); default_max_piece = 4; rotate_allowed = true; }}, {"Cube", "spherical level geometry", SPHERICAL, [] { set_geometry(gSmallSphere); set_variation(eVariation::pure); set_geometry(gProduct); default_max_piece = 3; rotate_allowed = false; }}, {"Klein bottle", "non-orientable manifold", EUCLIDEAN | NONORIENTABLE, [] { auto& T0 = euc::eu_input.user_axes; T0[0][0] = 5; T0[0][1] = 0; T0[1][0] = 0; T0[1][1] = 5; euc::eu_input.twisted = 8; set_geometry(gEuclidSquare); set_variation(eVariation::pure); set_geometry(gProduct); default_max_piece = 4; rotate_allowed = true; }}, {"pentagons", "different tiles on the Bring surface", HYPERBOLIC | HDUAL, [] { using namespace fieldpattern; current_extra = 2; auto& gxcur = fgeomextras[current_extra]; while(isize(gxcur.primes) < 1) nextPrime(gxcur); fgeomextras[current_extra].current_prime_id = 0; enableFieldChange(); set_geometry(gFieldQuotient); set_variation(eVariation::pure); set_geometry(gProduct); default_max_piece = 4; rotate_allowed = false; }}, {"double cube", "six squares around a vertex", HYPERBOLIC, [] { using namespace fieldpattern; current_extra = 3; auto& gxcur = fgeomextras[current_extra]; while(isize(gxcur.primes) < 1) nextPrime(gxcur); fgeomextras[current_extra].current_prime_id = 0; enableFieldChange(); set_geometry(gFieldQuotient); gp::param = gp::loc(1, 1); set_variation(eVariation::unrectified); set_geometry(gProduct); default_max_piece = 3; rotate_allowed = true; }}, {"30/6", "six squares around a vertex", HYPERBOLIC, [] { using namespace fieldpattern; current_extra = 3; auto& gxcur = fgeomextras[current_extra]; while(isize(gxcur.primes) < 1) nextPrime(gxcur); fgeomextras[current_extra].current_prime_id = 2; enableFieldChange(); set_geometry(gFieldQuotient); gp::param = gp::loc(1, 1); set_variation(eVariation::unrectified); set_geometry(gProduct); default_max_piece = 4; rotate_allowed = true; }}, {"42", "seven squares around a vertex", HYPERBOLIC, [] { using namespace fieldpattern; current_extra = 4; auto& gxcur = fgeomextras[current_extra]; while(isize(gxcur.primes) < 1) nextPrime(gxcur); fgeomextras[current_extra].current_prime_id = 0; enableFieldChange(); set_geometry(gFieldQuotient); gp::param = gp::loc(1, 1); set_variation(eVariation::unrectified); set_geometry(gProduct); default_max_piece = 4; rotate_allowed = false; }}, {"bounded well", "five squares around a vertex", BOUNDED_WELL, [] { set_geometry(g45); gp::param = gp::loc(1, 1); set_variation(eVariation::unrectified); set_geometry(gProduct); default_max_piece = 4; rotate_allowed = false; }}, {"mirrored Bring", "hyperbolic and non-orientable", HYPERBOLIC | NONORIENTABLE | ASYMMETRIC_ONLY, [] { set_geometry(gBring); gp::param = gp::loc(1, 1); set_variation(eVariation::unrectified); start_game(); subquotient::create_subquotient(2); set_geometry(gProduct); default_max_piece = 4; rotate_allowed = false; }}, {"giant", "like mirrored Bring but much larger", HYPERBOLIC | NONORIENTABLE, [] { using namespace fieldpattern; current_extra = 2; auto& gxcur = fgeomextras[current_extra]; while(isize(gxcur.primes) < 1) nextPrime(gxcur); fgeomextras[current_extra].current_prime_id = 1; enableFieldChange(); set_geometry(gFieldQuotient); gp::param = gp::loc(1, 1); set_variation(eVariation::unrectified); subquotient::create_subquotient(2); set_geometry(gProduct); default_max_piece = 5; rotate_allowed = false; well_size = 6; }}, {"orbifold", "one fifth of the giant", HYPERBOLIC | NONORIENTABLE | ORBIFOLD, [] { using namespace fieldpattern; current_extra = 2; auto& gxcur = fgeomextras[current_extra]; while(isize(gxcur.primes) < 1) nextPrime(gxcur); fgeomextras[current_extra].current_prime_id = 1; enableFieldChange(); set_geometry(gFieldQuotient); gp::param = gp::loc(1, 1); set_variation(eVariation::unrectified); subquotient::create_subquotient(10); set_geometry(gProduct); default_max_piece = 4; rotate_allowed = false; }}, {"torus: shear", "Nil geometry: are you sure you want this?", SECRET, [] { nilv::nilperiod = make_array(5, 0, 5); // nilv::set_flags(); set_geometry(gNil); default_max_piece = 4; rotate_allowed = false; }}, #if CAP_SOLV {"torus: Arnold's Cat", "Solv geometry: flat shapes are crazy enough", SECRET | FLAT_ONLY, [] { asonov::period_xy = 5; asonov::period_z = 0; asonov::set_flags(); set_geometry(gArnoldCat); default_max_piece = 2; rotate_allowed = false; }}, #endif }; void create_game(); void enable_bgeom() { stop_game_and_switch_mode(rg::nothing); well_size = 10; bgeoms[bgeom].create(); max_piece = default_max_piece; start_game(); create_game(); state = tsPreGame; } void enable_bgeom(int b) { bgeom = b; enable_bgeom(); } flagtype bflags() { return bgeoms[bgeom].flags; } using code_t = vector>; struct piecedata { code_t code; int multi; int penalty; int symmetries; int count; }; vector piecelist; map, int> seen_blocks; bool listed(const vector& v, cell* c) { for(auto cw: v) if(cw.at == c) return true; return false; } int down_dir() { return nil ? 4 : sol ? 0 : cwt.at->type-1; } int up_dir() { return nil ? 1 : sol ? 6 : cwt.at->type-2; } cell *get_at(cell *lev, int z) { if(mproduct) return hybrid::get_at(lev, z); else { // auto co = nilv::get_coord(lev->master); // co[2] += z * co[0]; // co[1] = z; // return nilv::get_heptagon_at(co)->c7; while(z<0) z++, lev=lev->cmove(up_dir()); while(z>0) z--, lev=lev->cmove(down_dir()); return lev; } } int get_z(cell* c) { if(mproduct) return hybrid::get_where(c).second; else if(nil) return nilv::get_coord(c->master)[1]; else if(sol) return asonov::get_coord(c->master)[2]; else exit(1); } pair get_where(cell *what) { if(mproduct) return hybrid::get_where(what); else { int z = get_z(what); int zm = z; while(zm>0) what = what->cmove(up_dir()), zm--; while(zm<0) what = what->cmove(down_dir()), zm++; return {what, z}; // co[2] -= co[1] * co[0]; // co[1] = 0; // cell *c = nilv::get_heptagon_at(co)->c7; // return {c, z}; } } set as_set(const vector& shape) { set res; for(auto sh: shape) res.insert(sh.at); return res; } bool same(const vector& shape, const vector& shape2) { set allcells; if(isize(shape) != isize(shape2)) return false; for(auto sh: shape) allcells.insert(sh.at); for(auto sh: shape2) allcells.erase(sh.at); return allcells.empty(); } cellwalker flatspin(cellwalker cw, int i) { if(solnil) cw.spin = i; else cw.spin = gmod(cw.spin + (cw.mirrored ? -i : i), cw.at->type - (mhybrid ? 2 : 0)); return cw; } int add_dir(cellwalker orig, int i, int sym = 0) { if(mproduct) { if(i >= orig.at->type-2) { if(sym&2) i ^= (orig.at->type-1) ^ (orig.at->type-2); return i; } else { if(sym&1) i = -i; return flatspin(orig, i).spin; } } return i; } cellwalker add(cellwalker orig, int i, int sym = 0) { if(mproduct) { if(i >= orig.at->type-2) { if(sym&2) i ^= (orig.at->type-1) ^ (orig.at->type-2); orig.at = orig.at->cmove(i); return orig; } else { if(sym&1) i = -i; return flatspin(orig, i) + wstep; } } return orig + i + wstep; } vector build_from(const code_t& code, cellwalker start, int sym = 0) { vector all = {start}; for(auto c: code) all.push_back(add(all[c.first], c.second, sym)); return all; } vector build_shape_matrices(const code_t& code, cellwalker start, int sym = 0) { vector all = {start}; vector allm = {Id}; for(auto c: code) { all.push_back(add(all[c.first], c.second, sym)); allm.push_back(allm[c.first] * currentmap->adj(all[c.first].at, add_dir(all[c.first], c.second, sym))); } return allm; } int penalty(const vector& shape, const code_t& code) { int p = 0; if(mproduct) { int bad = shape[0].at->type-1; for(auto co: code) if(co.second == bad) p += 1000; } map dists; for(auto cw: shape) dists[cw.at] = 99; dists[shape[0].at] = 0; for(int i=0; i get_z(shape[0].at)) p += 10000; if(bflags() & HDUAL) { for(auto s: shape) if(get_z(s.at) > get_z(shape[0].at)) p += 20000; /* if(!dists.count(shape[0].at->move(2))) p += 40000; if(!dists.count(shape[0].at->move(3))) p += 20000; */ } return p; } bool builds(const vector& shape, const code_t& code, int sym = 0, int eliminate = -1) { if(isize(shape) != isize(code)+1) return false; int ori = (solnil) ? 1 : mproduct ? shape[0].at->type-2 : shape[0].at->type; for(auto sh: shape) for(int i=0; i shape2 = build_from(code, cellwalker(sh.at, i), sym); if(eliminate != -1) seen_blocks.emplace(as_set(shape2), eliminate); if(same(shape, shape2)) return true; } return false; } void generate_shapes_rec(vector& sofar, code_t& code, int cnt) { if(isize(sofar) == cnt) { int p = penalty(sofar, code); auto ass = as_set(sofar); if(seen_blocks.count(ass)) { auto& pc = piecelist[seen_blocks[ass]]; if(p < pc.penalty) pc.penalty = p, pc.code = code; pc.multi++; return; } int id = 0; for(auto& pc: piecelist) { if(builds(sofar, pc.code, 0, id)) { if(p < pc.penalty) pc.penalty = p, pc.code = code; pc.multi++; return; } id++; } int syms = 0; bool invalid = false; for(int i: {0,1,2,3}) if(builds(sofar, code, i)) { syms++; if((bflags() & ASYMMETRIC_ONLY) && i == 1 && cnt >= 4) invalid = true; } if(invalid) return; bool vertical = true; for(auto c: code) if(c.second < 4) vertical = false; if(vertical) syms *= 2; if(isize(code) == 0) syms = 1; piecelist.push_back(piecedata{code, 1, p, syms, 0}); return; } for(int i=0; itype; t++) { if(sol && !among(t, 4, 5, 10, 11)) continue; cellwalker ncw = add(sofar[i], t); if(listed(sofar, ncw.at)) continue; code.emplace_back(i, t); sofar.push_back(ncw); generate_shapes_rec(sofar, code, cnt); sofar.pop_back(); code.pop_back(); } } void generate_shapes(int cnt) { vector cws = { get_at(get_center(), -well_size - 1) }; code_t co = {}; generate_shapes_rec(cws, co, cnt); } void list_all() { println(hlog, "number of shapes = ", isize(piecelist)); // for(auto sh: piecelist) println(hlog, "multi=", sh.multi, " penalty=", sh.penalty, " syms=", sh.symmetries, " => ", sh.code); } void water_shape() { auto shape = build_from(piecelist[shape_id].code, at); for(auto c: shape) c.at->wall = waSea; } set to_disappear; color_t hipso[] = { 0x3030C0, 0x6060FF, 0x9090FF, 0xFFFFFF, 0x008000, 0x40FF00, 0xFFFF00, 0xFF8000, 0xFF0000, 0x800000, 0x400040, 0x500050, 0x600060 }; color_t get_hipso(ld y) { y += 12; if(well_size <= 5) y *= 2; return hipso[gmod(y, 13)]; } void draw_shape() { auto shape = build_from(piecelist[shape_id].code, at); for(auto c: shape) { int y = -get_z(c.at); c.at->wall = waWaxWall, c.at->landparam = get_hipso(y); } ray::reset_raycaster_map(); } bool shape_drawn() { auto shape = build_from(piecelist[shape_id].code, at); for(auto c: shape) return c.at->wall == waWaxWall; return false; } void remove_shape() { auto shape = build_from(piecelist[shape_id].code, at); for(auto c: shape) c.at->wall = waNone; ray::reset_raycaster_map(); } bool shape_conflict(cellwalker cw) { auto shape = build_from(piecelist[shape_id].code, cw); /* self-conflict possible in the orbifold */ for(int i=0; iwall) return true; return false; } bool check_bshift(cellwalker c0, cellwalker c1) { if(c0.at->type != 6) return false; auto shape0 = build_from(piecelist[shape_id].code, c0); auto shape1 = build_from(piecelist[shape_id].code, c1); for(int i=0; iwall) continue; if(shape1[i].at->cmove(up_dir())->wall) return true; } } return false; } ld current_move_time_limit() { // return 50000 * pow(.9, completed) + 10000. / (1 + completed); return 3500 * pow(.995, completed * isize(level)); } int turn_animation = 500; int collect_animation = 300; int choose_piece() { ld total = 0; int sel = 0; ld lowest = 100000; for(auto &p: piecelist) lowest = min(lowest, p.count * 1. / p.symmetries); vector> probs; for(int i=0; i by_level; void find_lines() { by_level.clear(); // println(hlog, "Removing levels"); for(int z=1; z<=well_size; z++) { int ct = 0; for(auto lev: level) { cell *c = get_at(lev, -z); ct += (c->wall ? 1 : 0); } by_level.push_back(ct); } // println(hlog, by_level, " / ", isize(level)); int points = 0; if(true) { for(int z=1; z<=well_size; z++) if(by_level[z-1] >= isize(level)) { points++; for(auto lev: level) { cell *c = get_at(lev, -z); to_disappear.insert(c); } } } else { // int lines_found = 0; for(int z=1; z<=well_size; z++) { for(auto lev: level) for(int d=0; dtype; d++) { cellwalker cw(get_at(lev, -z), d); cellwalker cw0 = cw; bool filled = true; do { if(!cw.at->wall) filled = false; cw += wstep; cw = flatspin(cw, lev->type/2); } while(cw != cw0); if(filled) { // lines_found = true; do { to_disappear.insert(cw.at); cw += wstep; cw = flatspin(cw, lev->type/2); } while(cw != cw0); } } } // println(hlog, "lines found = ", lines_found); } if(!to_disappear.empty()) { move_at = ticks + collect_animation; state = tsCollect; score += 100000. * points * (points+1.) / current_move_time_limit(); completed += points; playSound(cwt.at, points == 1 ? "pickup-gold" : "orb-mind"); if(points == 4 && pro_game && max_piece == 4 && default_max_piece == 4) rv_achievement("BRINGRISFOUR"); } } void disappear_lines() { for(auto lev: level) { int nz = 1; for(int z=1; z<=well_size; z++) { cell *c1 = get_at(lev, -z); if(!to_disappear.count(c1)) { cell *c0 = get_at(lev, -nz); c0->wall = c1->wall; c0->landparam = get_hipso(nz); nz++; } } while(nz <= camera_level) { cell *c0 = get_at(lev, -nz); c0->wall = waNone; nz++; } } to_disappear.clear(); state = tsBetween; ray::reset_raycaster_map(); } void state_loop() { find_lines(); if(to_disappear.empty()) new_piece(); } void fallen() { draw_shape(); bricks++; cubes += isize(piecelist[shape_id].code)+1; state = tsBetween; playSound(cwt.at, "closegate"); score += 200000. / (current_move_time_limit() * 3 + ticks - move_started); } void drop() { remove_shape(); cellwalker fall = at; fall.at = fall.at->cmove(down_dir()); if(shape_conflict(fall)) fallen(); else { at = fall; draw_shape(); } if(solnil) { pView = pView * currentmap->adj(cwt.at, down_dir()); when_t = ticks + turn_animation; } } void auto_drop() { drop(); move_at = ticks + current_move_time_limit(); } void fulldrop() { remove_shape(); cellwalker fall = at; int no = 0; auto last = fall; while(true) { last = fall; fall.at = fall.at->cmove(down_dir()); if(shape_conflict(fall)) break; no++; } playSound(cwt.at, "hit-crush2"); // println(hlog, "dropped by ", no); fall = last; at = fall; move_at = ticks + current_move_time_limit(); draw_shape(); if(!no) fallen(); } void verify_matrix(transmatrix T) { vector ver; for(int i=0; i<3; i++) for(int j=0; j<3; j++) { ld a = T[0][i] * T[0][j] + T[1][i]*T[1][j] - T[2][i] * T[2][j]; ver.push_back(kz(a)); } println(hlog, ver); } ld move_dist; void set_view() { move_dist = hdist0(currentmap->adj(cwt.at, 0) * C0); if(in_h2xe() && PURE && S3 == 4) { ld dist = PIU(hdist0(get_corner_position(currentmap->gamestart(), 0))); dist -= 1e-4; move_dist = PIU(hdist(get_corner_position(currentmap->gamestart(), 0), get_corner_position(currentmap->gamestart(), 1))); tView = xpush(-dist) * tView; tView = spin(135._deg) * tView; } if(in_h2xe() && UNRECTIFIED) tView = spin(135._deg) * tView; if(in_h2xe() && S7 == 4) tView = spin90() * tView; if(in_s2xe()) tView = spin90() * tView; if(in_e2xe()) tView = spin90() * tView; } void set_tview(transmatrix T) { View = T * tView; transmatrix rel = pView * inverse(View); fixmatrix(rel); centerover = ncenter; optimizeview(); ncenter = centerover; // tView = View; if(bgeom == 4) tView = spin(72._deg*at.spin); else tView = spin(90._deg*at.spin); if(at.mirrored) tView = MirrorY * tView; // tView = spin(90._deg*at.spin); set_view(); pView = rel * tView; when_t = ticks + turn_animation; } void rotate_block(int d, bool camera_only) { if(!rotate_allowed && !camera_only) { playSound(cwt.at, "hit-crush3"); return; } if(!camera_only) remove_shape(); cellwalker at1 = flatspin(at, d); if(camera_only || !shape_conflict(at1)) { at = at1; set_tview(spin(d*90._deg)); } else playSound(cwt.at, "hit-crush3"); if(!camera_only) draw_shape(); } int nilmap(int dir) { if(nil) { int nm[4] = {3, 2, 0, 5}; return nm[dir]; } if(sol) { int nm[4] = {11, 10, 5, 4}; return nm[dir]; } exit(1); } cell *shift_block_target(int dir) { return flatspin(at, dir).cpeek(); } void shift_block(int dir, bool camera_only) { int t = currentmap->gamestart()->type; if(mproduct) t -= 2; if(!camera_only) remove_shape(); cellwalker at1; if(solnil) { at1.at = at.at->cmove(nilmap(dir)); } else if(t&1) { if(dir == 3) at1 = flatspin(flatspin(at, 4) + wstep, 3); if(dir == 1) at1 = flatspin(flatspin(at, 2) + wstep, 1); if(dir == 2) at1 = flatspin(flatspin(at, 3) + wstep, 4); if(dir == 0) at1 = flatspin(flatspin(at, 1) + wstep, 2); } else { int kspin = (t/2) - dir; at1 = flatspin(at, dir); at1 = flatspin(at1 + wstep, kspin); } ld angle = dir * 90 * degree; if(camera_only || !shape_conflict(at1)) { // playSound(cwt.at, "hit-crush1"); if(check_bshift(at, at1)) rv_achievement("BSHIFT"); at = at1; if(solnil) { pView = pView * currentmap->adj(cwt.at, nilmap(dir)); when_t = ticks + turn_animation; } else set_tview(spin(-angle) * ypush(-move_dist) * spin(angle)); if(!camera_only) draw_shape(); } else playSound(cwt.at, "hit-crush3"); } void bringris_action(int k) { if(k < 4) shift_block(k); if(k == 4) rotate_block(1); if(k == 5) rotate_block(-1); if(k == 6) drop(); if(k == 7) fulldrop(); if(k == 8) paused = true; } void create_matrices() { dq::clear_all(); make_actual_view(); dq::enqueue_by_matrix_c(centerover, cview()); current_display->all_drawn_copies.clear(); int id = 0; while(!dq::drawqueue_c.empty()) { auto& p = dq::drawqueue_c.front(); cell*& c = p.first; shiftmatrix& V = p.second; current_display->all_drawn_copies[c].push_back(V); gmatrix[p.first] = p.second; if(id < draw_per_level) { auto go = [&] (int i) { cell *c1 = c->cmove(i); dq::enqueue_by_matrix_c(c1, optimized_shift(V * currentmap->adj(c, i))); }; if(mproduct) { for(int i=0; itype-2; i++) go(i); } else if(sol) { go(4); go(5); go(10); go(11); } else if(nil) { go(0); go(2); go(3); go(5); } } dq::drawqueue_c.pop(); id++; } } transmatrix smooth; void change_depth(shiftmatrix& V, int newlevel, int zlev) { if(solnil) { while(newlevel > zlev) zlev++, V = V * currentmap->adj(cwt.at, down_dir()); while(newlevel < zlev) zlev--, V = V * currentmap->adj(cwt.at, up_dir()); } else if(in_h2xe()) V = shiftless(V.T, cgi.plevel * (newlevel - zlev)); else V = shiftless(V.T * zpush(cgi.plevel * (newlevel - zlev))); } void draw_wirecube_at(cell *c, const transmatrix& rel, int zlev, color_t col) { auto where_c = get_where(c); auto c_camera = get_at(where_c.first, zlev); for(shiftmatrix V: current_display->all_drawn_copies[c_camera]) { change_depth(V, where_c.second, zlev); for(int i=0; itype; i++) queuepolyat(V * rel, cgi.shWireframe3D[i], 0, PPR::SUPERLINE).outline = col; } } void draw_piece(int zlev, int id) { auto shape = build_from(piecelist[id].code, at); auto matrices = build_shape_matrices(piecelist[id].code, at); auto where_at = get_where(at.at); vid.linewidth *= 3; int mid = 0; for(auto c: shape) { auto where_c = get_where(c.at); color_t levels[5] = {color_t(0xFFFFFFFFF), color_t(0xFFFF00FF), color_t(0xFF8000FF), color_t(0xFF0000FF), color_t(0xC000C0FF) }; draw_wirecube_at(at.at, smooth * matrices[mid++], zlev, levels[where_at.second-where_c.second]); } vid.linewidth /= 3; } void draw_holes(int zlev) { bool d = state == tsFalling && shape_drawn(); if(d) remove_shape(); for(auto lev: level) { bool covered = false; for(int z=well_size; z>=1; z--) { cell *c1 = get_at(lev, -z); if(c1->wall) covered = true; else if(covered) { vid.linewidth *= 4; draw_wirecube_at(c1, Id, zlev, (get_hipso(z) << 8) | 0xFF); vid.linewidth /= 4; } } } if(d) draw_shape(); } void draw_all_noray(int zlev) { for(auto lev: level) { for(int z=0; z<=camera_level+1; z++) { cell *c1 = get_at(lev, -z); if(c1->wall) { auto c_camera = get_at(lev, zlev); for(shiftmatrix V: current_display->all_drawn_copies[c_camera]) { change_depth(V, -z, zlev); color_t wcol = c1->landparam; if(c1->wall == waBarrier) wcol = winf[waBarrier].color; int d = (wcol & 0xF0F0F0) >> 4; forCellIdCM(c2, i, c1) if(!c2->wall) { color_t col = darkena(wcol - d * get_darkval(c1, i), 0, 0xFF); auto &q = queuepolyat(V, cgi.shWall3D[i], col, PPR::WALL); q.tinf = &floor_texture_vertices[cgi.shFloor.id]; ensure_vertex_number(*q.tinf, q.cnt); } } } } } } bool use_equidistant; void bringris_frame() { if(!in_bringris) return; ray::want_use = use_raycaster ? 2 : 0; #if CAP_VR vrhr::hsm = explore ? vrhr::eHeadset::holonomy : vrhr::eHeadset::reference; vrhr::eyes = use_equidistant ? vrhr::eEyes::equidistant : vrhr::eEyes::truesim; #endif int zlev = get_z(centerover); if(state == tsCollect) for(cell *c: to_disappear) c->landparam = flashes ? rand() & 0xFFFFFF : 0x101010; // just_gmatrix = true; create_matrices(); if(!use_raycaster) draw_all_noray(zlev); if(anyshiftclick) draw_holes(zlev); if(state == tsFalling && !explore && !cur_ang) draw_piece(zlev, shape_id); } renderbuffer *next_buffer; void draw_screen(int xstart, bool show_next) { int steps = camera_level - (-get_z(at.at)); if(state != tsFalling) steps = camera_level - (well_size + 1); #if CAP_VR if(!explore) { E4; vrhr::hmd_at_ui = vrhr::hmd_ref_at * cspin(0, 2, 30._deg); } #endif dynamicval ccd(*current_display); current_display->xmax = xstart * 1. / vid.xres; ray::max_cells = (isize(level) + isize(out_level)) * (camera_level+2); if(explore) { gamescreen(); lps_enable(&lps_bringris_explore); } else { lps_enable(&lps_bringris_play); NLP = Id; View = pView; if(nil) { centerover = at.at; rotate_view(cspin90(2, 1)); shift_view(ztangent(3 * nilv::nilwidth)); rotate_view(cspin90(1, 0)); anims::moved(); } else if(sol) { centerover = at.at; rotate_view(cspin180(1, 2)); shift_view(ztangent(1)); rotate_view(cspin90(1, 0)); anims::moved(); } else { ld lv = -cgi.plevel * steps; shift_view(ztangent(lv)); rotate_view(cspin(1, 2, cur_ang)); shift_view(ztangent(cgi.plevel * (2 + max_piece))); centerover = ncenter; anims::moved(); } // make_actual_view(); // anims::moved(); sightranges[geometry] *= 100; if(state == tsFalling && !explore && !cur_ang && !lctrlclick) remove_shape(); gamescreen(); if(state == tsFalling && !explore && !cur_ang) draw_shape(); extern void render_next(int xstart); render_next(xstart); sightranges[geometry] /= 100; if(state == tsBetween) state_loop(); if(state == tsCollect && ticks >= move_at) disappear_lines(); if(ticks >= move_at && state == tsFalling && pro_game) { auto_drop(); } View = pView; centerover = ncenter; NLP = Id; } } void create_game(); void geometry_menu() { clearMessages(); dialog::init("Bringris geometries"); dialog::addBreak(100); for(int i=0; i g(geometry, gNormal); saveConfig(); }); #endif dialog::addBack(); dialog::display(); } void adjust_animation(ld part) { if(solnil) { hyperpoint sh = pView * C0; sh = lerp(C0, sh, 1-part); pView = eupush(sh); smooth = inverse(pView); } else { transmatrix T = pView * inverse(tView); hyperpoint vec = inverse_exp(shiftless(tC0(T))); transmatrix Tspin = gpushxto0(tC0(T)) * T; ld alpha = atan2(Tspin*xpush0(1)); println(hlog, "vec=", vec, " part = ", part); pView = spin(alpha * part) * gpushxto0(direct_exp(vec*part)) * pView; fixmatrix(pView); View = tView; smooth = inverse(pView) * cview().T; } } bool next_fail = false; int TEXTURESIZE = 256; int nxmin, nxmax, nymin, nymax; void render_next(int xstart) { if(!next_buffer && !next_fail) { next_buffer = new renderbuffer(TEXTURESIZE, TEXTURESIZE, true); if(!next_buffer->valid) { next_fail = true; delete next_buffer; next_buffer = nullptr; println(hlog, "failed to create next_buffer"); } else println(hlog, "valid next_buffer created"); } nxmin = (xstart + vid.fsize); nxmax = (vid.xres - vid.fsize); nymin = vid.yres - (vid.fsize * (18+8)); nymax = vid.yres - (vid.fsize * 18); int zlev = get_z(centerover); if(!next_buffer) { if(1) { dynamicval ccd(*current_display); current_display->xmin = nxmin * 1. / vid.xres; current_display->xmax = nxmax * 1. / vid.xres; current_display->ymin = 1 - nymax * 1. / vid.yres; current_display->ymax = 1 - nymin * 1. / vid.yres; calcparam(); initquickqueue(); draw_piece(zlev, next_shape_id); quickqueue(); } calcparam(); return; } if(1) { resetbuffer rb; next_buffer->enable(); dynamicval dx(vid.xres, TEXTURESIZE); dynamicval dy(vid.yres, TEXTURESIZE); dynamicval dxmi(current_display->xmin, 0); dynamicval dxma(current_display->xmax, 1); dynamicval dymi(current_display->ymin, 0); dynamicval dyma(current_display->ymax, 1); #if CAP_VR dynamicval dvr(vrhr::state, 0); #endif calcparam(); current_display->set_viewport(0); setGLProjection(); // gamescreen(1); initquickqueue(); draw_piece(zlev, next_shape_id); quickqueue(); rb.reset(); } } EX void reset_vr_ref() { #if CAP_VR vrhr::hmd_ref_at = vrhr::hmd_at_ui = vrhr::hmd_at; #endif } EX void display_next(int xstart) { if(next_buffer) { current_display->next_shader_flags = GF_TEXTURE; dynamicval m(pmodel, mdPixel); current_display->set_all(0, 0); glBindTexture(GL_TEXTURE_2D, next_buffer->renderedTexture); glhr::id_modelview(); glhr::set_depthtest(false); glhr::color2(0xFFFFFFFF); vector tvx; for(int a=0; a<6; a++) { int dx[6] = {0, 1, 1, 0, 0, 1}; int dy[6] = {0, 0, 1, 0, 1, 1}; glhr::textured_vertex tx; tx.coords[2] = 0; tx.coords[3] = 1; tx.coords[0] = (dx[a] ? nxmax : nxmin) - current_display->xcenter; tx.coords[1] = (dy[a] ? nymax : nymin) - current_display->ycenter; tx.texture[0] = dx[a]; tx.texture[1] = dy[a]; tvx.push_back(tx); } glhr::prepare(tvx); glDrawArrays(GL_TRIANGLES, 0, 6); } } purehookset bringris_extensions; void run() { clearMessages(); dialog::init(); if(ang < cur_ang) { cur_ang -= (ticks - lti) / 1000.; if(cur_ang < ang) cur_ang = ang; } if(ang > cur_ang) { cur_ang += (ticks - lti) / 1000.; if(cur_ang > ang) cur_ang = ang; } lti = ticks; if(explore) ; else if(ticks > when_t) { pView = tView; View = pView; smooth = Id; } else adjust_animation((ticks - last_adjust) * 1. / (when_t - last_adjust)); last_adjust = ticks; ray::want_use = use_raycaster ? 2 : 0; sightranges[geometry] = 50; if(!solnil) vid.cells_drawn_limit = 1; else vid.cells_drawn_limit = 2000; cmode = sm::NORMAL | sm::CENTER; if(!explore) cmode |= sm::VR_MENU; int xstart = vid.xres; if(!nohud) xstart -= vid.fsize * 10; getcstat = '-'; bool show_next = state != tsGameover && state != tsPreGame && !paused; #if CAP_VR dynamicval d(vrhr::change_ui_bounds, [show_next, xstart] { vrhr::ui_xmin = xstart; vrhr::ui_xmax = vid.xres; if(show_next && !explore) vrhr::ui_ymin = vid.yres - vid.fsize * 28; }); #endif draw_screen(xstart, show_next); calcparam(); bool in_menu = !show_next && !explore; if(nohud) { describeMouseover(); } else { for(int i=0; i 3) ax = 3; int ay = mousey * 3 / vid.yres; if(ay > 2) ay = 2; int id = ay * 4 + ax; eBringrisMove moves[12] = { bmTurnLeft, bmUp, bmTurnRight, bmPause, bmLeft, bmDrop, bmRight, bmFullDrop, bmNothing, bmDown, bmNothing, bmFullDrop }; eBringrisMove mov = moves[id]; if((state == tsFalling && !paused) || mov == bmPause) bringris_action(mov); } #if CAP_VR else { if(explore) explore = !explore; else if( mousex >= vrhr::ui_xmin && mousex <= vrhr::ui_xmax && mousey >= vrhr::ui_ymin && mousey <= vrhr::ui_ymax) paused = !paused; else { vrhr::compute_vr_direction(which_pointer); ld r = 1 / sqrt(2); auto& dir = vrhr::vr_direction; vector > choices = { {bmLeft, hyperpoint(-1, 0, 0, 0)}, {bmRight, hyperpoint(1, 0, 0, 0)}, {bmUp, hyperpoint(0, -1, 0, 0)}, {bmDown, hyperpoint(0, 1, 0, 0)}, {bmTurnLeft, hyperpoint(-r, -r, 0, 0)}, {bmTurnRight, hyperpoint(r, -r, 0, 0)}, {bmDrop, hyperpoint(-r, r, 0, 0)}, {bmFullDrop, hyperpoint(r, r, 0, 0)} }; eBringrisMove mov = bmNothing; ld best = 0; for(auto& b: choices) { ld dot = (dir | b.second); if(dot > best) best = dot, mov = b.first; } bringris_action(mov); } println(hlog, vrhr::vr_direction); } #endif return; } // if(sym == 'k') ang = 0; // if(sym == 'l') ang = 45 * degree; if(sym == 'p' || sym == 'c' || (sym == SDLK_ESCAPE && !ISWEB)) { if(!paused) move_at = move_at - ticks; paused = !paused; if(!paused) move_at = move_at - ticks; explore = false; } if(sym == 't' && state == tsGameover) { const vector emoji = {"😀","😎","👽","🤖","😺","🎩","🎓","👑","💍","🐯","🦁","🐮","🐷","🐽","🐸","🐙","🐵","🐦","🐧","🐔","🐒","🙉","🙈","🐣","🐥","🐺","🐗","🐴","🦄","🐝","🐛","🐢","🦀","🦂","🕷","🐜","🐞","🐌","🐠","🐟","🐡","🐬","🐋","🐊","🐆","🐘","🐫","🐪","🐄","🐂","🐃","🐏","🐑","🐀","🐁","🐓","🦃","🐉","🐾","🐿","🐇","🐈","🐩","🐕","🐲","🌵","🍁","🌻","🌎","⭐️","⚡️","🔥","❄️","☔️","☂️","💧","🍏","🍎","🍐","🍋","🍌","🍉","🍇","🌶","🍅","🍍","🍑","🍈","🍓","🌽","🍠","🍯","🍞","🍗","🧀","🍖","🍤","🌯","🌮","🍝","🍕","🌭","🍟","🍔","⚽️","🎱","🏆","🎪","🎲","🎳","🚗","🚕","🚙","🏎","⛺️","⛩","🕹","💾","☎️","⏱","🔦","💡","💰","💎","🔨","💣","🔑","❤️","🔔"}; string out; if(pro_game) { out = "Got " + its(score) + " points for completing " + its(completed) + " levels in #Bringris!"; } else if(completed) { out = "Used " + its(bricks) + " blocks to complete " + its(completed) + " levels in #Bringris!"; } else { out = "Dropped " + its(bricks) + " blocks in #Bringris!"; } if(bgeom || max_piece != 4) out += " (" + bgeoms[bgeom].name + "/" + its(max_piece) + ")"; unsigned hash = time(NULL) / 600; for(char c: out) hash = 171 * hash + c; std::mt19937 invr; invr.seed(hash); out += " "; for(int i=0; i<4; i++) out += emoji[invr() % isize(emoji)]; // println(hlog, out); #if ISWEB EM_ASM({ var tweetbegin = 'https://twitter.com/intent/tweet?text='; var tweettxt = UTF8ToString($0, $1); var finaltweet = tweetbegin +encodeURIComponent(tweettxt) + "&url=https://zenorogue.itch.io/bringris"; window.open(finaltweet,'_blank'); }, out.c_str(), isize(out)); #endif } if(in_menu && sym == 'e') { explore = !explore; } if(sym == 'V') { reset_vr_ref(); } if(in_menu && sym == 'n') { start_new_game(); paused = false; explore = false; pro_game = false; playSound(cwt.at, "elementalgem"); } if(in_menu && sym == 's') { pushScreen(settings_menu); } if(in_menu && sym == 'x') { start_new_game(); paused = false; explore = false; pro_game = true; playSound(cwt.at, "elementalgem"); } #if CAP_SHOT if(sym == ']') { static int id = 0; perfect_linewidth = 0; shot::shot_aa = 2; vid.linewidth *= 2; shot::take(hr::format("bringris-%04d.png", id++), [] { draw_screen(vid.xres, false); }); vid.linewidth /= 2; } #endif #ifdef MAKE_VIDEO if(sym == '[') { perfect_linewidth = 0; shot::shot_aa = 4; vid.linewidth *= 5; int dir = 3; int fpm = in_s2xe() ? 60 : 30; int steps = 0; auto att = at; bool boundedw = bflags() & BOUNDED_WELL; if(boundedw) steps = 20; else do { steps++; shift_block(dir); } while(at != att); println(hlog, steps, " steps of ", fpm, " frames each"); anims::noframes = steps * fpm; anims::period = steps * fpm; int lt = -1; int p = addHook(anims::hooks_anim, 200, [&] { int t = ticks; if(t == lt) { println(hlog, "ignore"); return; } lt = t; println(hlog, "t = ", t); if(t % fpm == 0) { pView = tView; View = pView; smooth = Id; shift_block(dir); if(boundedw) dir = (1+dir) % 4; } if(boundedw) { auto f = [fpm] (ld k) { k /= fpm; return k * k * (3 - 2 * k); }; ld a = f(t % fpm); ld b = f(t % fpm + 1); adjust_animation((b-a) / (1-a)); } else adjust_animation(1. / (fpm - t % fpm)); }); anims::record_video("bringris.mp4", [] { return anims::record_animation_of( [] { println(hlog, "called"); draw_screen(vid.xres, false); } ); }); delHook(anims::hooks_anim, p); vid.linewidth /= 5; } #endif }; callhooks(bringris_extensions); } cell *get_center() { return well_center ? well_center : level[0]; } void reset_view() { centerover = get_at(get_center(), -camera_level); cwt.at = centerover; ncenter = get_at(get_center(), -camera_level); NLP = Id; tView = Id; set_view(); pView = tView; } void start_new_game() { timerstart = time(NULL); for(auto& p: piecelist) p.count = 0; for(auto lev: level) for(int z=0; z<=camera_level+1; z++) { cell *c = get_at(lev, -z); setdist(c, 7, nullptr); c->item = itNone; c->land = laCanvas; if(z == 0) c->wall = waBarrier, c->land = laBarrier; else if(z <= camera_level) c->wall = waNone; else c->wall = waWaxWall, c->land = laCanvas, c->landparam = 0xC000C0; } for(auto lev: out_level) for(int z=1; z<=camera_level; z++) { cell *c = get_at(lev, -z); c->item = itNone; c->land = laCanvas; c->wall = waWaxWall; c->landparam = (get_hipso(z) & 0xFCFCFC) >> 2; } at = get_at(get_center(), -well_size - 1); next_shape_id = choose_piece(); state = tsBetween; reset_view(); ray::reset_raycaster_map(); // reset_view(); completed = 0; bricks = 0; cubes = 0; score = 0; } void get_level() { if(bflags() & BOUNDED_WELL) { set all; well_center = currentmap->gamestart(); all.insert(well_center); for(int i=0; i<4; i++) for(int l: {-1, 0, 1}) for(int j=0; j<4; j++) for(int k: {-1, 1}) { cellwalker cw(well_center, i); cw += wstep; all.insert(cw.at); if(l) { cw += l; cw += wstep; all.insert(cw.at); } cw += j; cw += wstep; all.insert(cw.at); cw += k; cw += wstep; all.insert(cw.at); } set all_ext; for(cell *c: all) forCellCM(d, c) if(!all.count(d)) all_ext.insert(d); level.clear(); for(auto c: all) level.push_back(c); for(auto c: all_ext) out_level.push_back(c); } else { level = currentmap->allcells(); if(bflags() & ORBIFOLD) { vector clist; set visited; auto visit = [&] (cell *c) { if(!visited.count(c)) visited.insert(c), clist.push_back(c); }; for(auto c: level) if(isNeighbor(c, c)) visit(c); for(int i=0; itype; j++) visit(clist[i]->cmove(j)); well_center = clist.back(); } } if(well_center) { vector visited; set all; for(auto l: level) all.insert(l); auto visit = [&] (cell *c, int d) { if(all.count(c) && !center_distance.count(c)) center_distance[c] = d, visited.push_back(c); }; visit(well_center, 0); for(int i=0; itype; j++) visit(visited[i]->move(j), center_distance[visited[i]] + 1); } } void create_game() { level.clear(); out_level.clear(); well_center = nullptr; if(!mproduct && !solnil) { println(hlog, "need product or Solnil geometry"); exit(1); } if(nil) { for(int x=0; x<5; x++) for(int y=0; y<5; y++) level.push_back(nilv::get_heptagon_at(nilv::mvec(x, 0, y))->c7); } else if(sol) { level.clear(); for(int x=0; x<5; x++) for(int y=0; y<5; y++) level.push_back(asonov::get_at(asonov::coord(x, y, 0))->c7); } else PIU(get_level()); piecelist.clear(); piecelist.reserve(2000); seen_blocks.clear(); for(int ps=1; ps<=max_piece; ps++) generate_shapes(ps); list_all(); // println(hlog, "level size = ", isize(level)); camera_level = well_size + max_piece + camera; playermoved = false; ray::max_iter_current() = solnil ? 600 : 200; start_new_game(); state = tsPreGame; } void init_all() { lps_enable(&lps_bringris); enable_bgeom(); vid.texture_step = 8; showstartmenu = false; pushScreen(run); in_bringris = true; } int args() { using namespace arg; if(0) ; else if(argis("-list")) { PHASEFROM(3); start_game(); shift(); int i = argi(); generate_shapes(i); list_all(); } else if(argis("-bringris0")) { PHASEFROM(3); start_game(); create_game(); } else if(argis("-bgeo")) { PHASEFROM(2); shift(); enable_bgeom(argi()); } else if(argis("-bringris")) { PHASEFROM(2); init_all(); } else if(argis("-ray-off")) use_raycaster = false; else if(argis("-ray-on")) use_raycaster = true; else if(argis("-dpl")) { shift(); draw_per_level = argi(); } else return 1; return 0; } void change_default_key(int key, int val) { int* t = scfg_bringris.keyaction; t[key] = val; } void default_config() { clear_config(scfg_bringris); change_default_key('s', 16 + 0); change_default_key('a', 16 + 1); change_default_key('w', 16 + 2); change_default_key('d', 16 + 3); change_default_key('q', 16 + 4); change_default_key('e', 16 + 5); change_default_key(' ', 16 + 6); change_default_key('\r',16 + 7); change_default_key('p', 16 + 8); sconfig_savers(scfg_bringris, "bringris"); param_i(bgeom, "bringris-geometry", 0); lps_add(lps_bringris, ray::want_use, ray::want_use); #if CAP_VR lps_add(lps_bringris, vrhr::hsm, vrhr::hsm); lps_add(lps_bringris, vrhr::eyes, vrhr::eyes); #endif lps_add(lps_bringris, ray::exp_decay_poly, 200); lps_add(lps_bringris, ray::fixed_map, true); lps_add(lps_bringris, mapeditor::drawplayer, false); lps_add(lps_bringris, vid.fov, 90); lps_add(lps_bringris, vid.plevel_factor, 0.5); lps_add(lps_bringris, vid.axes3, false); lps_add(lps_bringris_explore, mouseaim_sensitivity, 0.01); lps_add(lps_bringris_explore, camera_speed, 2); lps_add(lps_bringris_explore, smooth_scrolling, true); lps_add(lps_bringris_play, mouseaim_sensitivity, 0); param_b(use_raycaster, "bringris-ray"); param_i(draw_per_level, "draw-per-level"); param_b(use_equidistant, "bringris-equidistant"); param_b(flashes, "bringris-flashes"); } auto hooks = addHook(hooks_args, 100, args) + addHook(hooks_frame, 100, bringris_frame) + addHook(hooks_configfile, 300, default_config) + addHook(dialog::hooks_display_dialog, 100, [] () { if(dialog::items[0].body == "Bringris keys") { dialog::addBreak(200); if(!rotate_allowed) dialog::addHelp("note: rotation keys only available when necessary"); dialog::addHelp("press SHIFT to highlight the holes"); dialog::addHelp("mouse control by pressing parts of the game screen"); } }); #ifdef BRINGRIS auto hook1= addHook(hooks_config, 100, [] { if(arg::curphase == 1) conffile = "bringris.ini"; if(arg::curphase == 2) init_all(); }); #endif void save() { #if CAP_SAVE fhstream f("bringris.save", "at"); println(f, "Bringris ", BRINGRIS_VER); println(f, bgeoms[bgeom].name); time_t timer; timer = time(NULL); char sbuf[128]; strftime(sbuf, 128, "%c", localtime(&timerstart)); char buf[128]; strftime(buf, 128, "%c", localtime(&timer)); println(f, sbuf); println(f, buf); println(f, max_piece, " ", pro_game ? score : -1, " ", bricks, " ", completed, " ", cubes, " ", well_size, " ", isize(level), " ", int(timer - timerstart)); for(int z=0; z<=well_size; z++) { string s; for(auto lev: level) { cell *c = get_at(lev, -z); s += (c->wall ? '#' : '.'); } println(f, s); } #endif } void load() { } } }