1
0
mirror of https://github.com/zenorogue/hyperrogue.git synced 2024-11-30 15:39:54 +00:00
hyperrogue/geom-exp.cpp

1271 lines
38 KiB
C++

// Hyperbolic Rogue -- the 'experiments with geometry' menu
// Copyright (C) 2011-2019 Zeno Rogue, see 'hyper.cpp' for details
/** \file geom-exp.cpp
* \brief The 'experiments with geometry' menu
*
* Implementation of this menu, and computation of the statistics shown there
*/
#include "hyper.h"
namespace hr {
int eupage = 0;
int euperpage = 21;
string euchelp =
"If you want to know how much the gameplay is affected by the "
"hyperbolic geometry in HyperRogue, this mode is for you!\n\n"
"You can try many different geometries here. We start by gluing "
"n-gons in such a way that k of them meet in every vertex. "
"Depending on n and k, this either folds into a sphere, unfolds into a plane, "
"or requires a hyperbolic space. The result may be then 'bitrunc' by "
"replacing each vertex by a 2k-gon. Furthermore, you can play "
"with quotient geometries. For example, the elliptic geometry is "
"obtained from the sphere by making the antipodes be the same point, "
"so you return to the same spot (but as a mirror image) after going there. "
"Have fun experimenting! "
"Achievements and leaderboards do not work in geometry experiments, "
"except some specific ones.\n\n"
"In standard geometry (bitrunc or not), you can play the full game, but in other geometries "
"you select a particular land. Lands are unlocked by visiting them in this "
"session, or permanently by collecting 25 treasure. Try Crossroads in Euclidean "
"or chaos mode in non-standard non-quotient hyperbolic to visit many lands. "
"Highlights:\n"
"* Crystal World and Warped Coast can be understood as extra geometries.\n"
"* Halloween is specially designed for spherical geometry.\n"
"* To see the difference, try Hunting Grounds in Euclidean -- it is impossible.\n";
#if CAP_FIELD
void showQuotientConfig() {
using namespace fieldpattern;
gamescreen(2);
dialog::init(XLAT("field quotient"));
fgeomextra& gxcur = fgeomextras[current_extra];
for(int i=0; i<isize(fgeomextras); i++) {
auto& g = fgeomextras[i];
dialog::addBoolItem(ginf[g.base].tiling_name, g.base == gxcur.base, 'a'+i);
dialog::add_action([i] { current_extra = i; });
}
dialog::addBreak(100);
nextPrimes(gxcur);
string stars[3] = {"", "*", "**"};
for(int i=0; i<isize(gxcur.primes); i++) {
auto& p = gxcur.primes[i];
int sides = ginf[gxcur.base].sides;
bool sa = shapename_available(sides);
string s = sa ? "order %1%2 (%3 %4s)" : "order %1%2 (%3 %4-gons)";
s = XLAT(s, its(p.p), p.squared ? "²" : "", its(p.cells), sa ? shapename(sides) : its(sides));
s += stars[gxcur.dualval[i]];
dialog::addBoolItem(s, i == gxcur.current_prime_id, 'A'+i);
dialog::add_action([&gxcur, i] { gxcur.current_prime_id = i; });
}
if(isize(gxcur.primes) <= 6) {
dialog::addBreak(100);
dialog::addHelp(
"This geometry is obtained by applying the same 'generators' which "
"lead to creating the given basic hyperbolic geometry, "
"but using a fixed finite field instead of the field of reals. "
"It can be also interpreted as a quotient of the given basic geometry. "
"Warning: field patterns based on large primes might generate for a long time."
);
dialog::addBreak(100);
}
dialog::addItem("find the next prime", 'p');
dialog::add_action([&gxcur] { nextPrime(gxcur); });
dialog::addItem("activate", 'x');
dialog::add_action_confirmed([&gxcur] {
set_geometry(gxcur.base);
enableFieldChange();
set_geometry(gFieldQuotient);
start_game();
});
dialog::addItem("find alternate manifolds", 'y');
dialog::add_action_confirmed([&gxcur] {
set_geometry(gxcur.base);
triplet_id = 0;
enableFieldChange();
set_geometry(gFieldQuotient);
start_game();
auto& cfp = currfp;
auto triplets = cfp.find_triplets();
pushScreen([triplets] {
gamescreen(2);
dialog::init(XLAT("alternate manifolds"));
int id = 0;
for(auto t: triplets) {
dialog::addItem(XLAT("generators (%1,%2), size %3", its(t.i), its(t.j), its(t.size/S7)), 'a'+id);
dialog::add_action([id] {
stop_game();
triplet_id = id;
fieldpattern::enableFieldChange();
start_game();
});
id++;
}
dialog::addBreak(100);
dialog::addHelp(XLAT(
"This option finds alternate solutions. For example, there are three {7,3} manifolds with 156 heptagons each (\"first Hurwitz triplet\").")
);
dialog::display();
});
});
dialog::addItem("default", 'z');
dialog::add_action_confirmed([] {
set_geometry(gEuclid);
fieldpattern::quotient_field_changed = false;
set_geometry(gFieldQuotient);
start_game();
});
dialog::addBack();
dialog::display();
}
#endif
EX string bitruncnames[5] = {" (b)", " (n)", " (g)", " (i)", " (d)"};
void validity_info() {
int vccolors[4] = {0xFF0000, 0xFF8000, 0xFFFF00, 0x00FF00};
auto lv = land_validity(specialland);
if(lv.flags & lv::display_error_message)
dialog::addInfo(XLAT(lv.msg), vccolors[lv.quality_level]);
else
dialog::addBreak(100);
}
EX bool showquotients;
string validclasses[4] = {" (X)", " (½)", "", " (!)"};
EX void ge_land_selection() {
cmode = sm::SIDE | sm::MAYDARK;
gamescreen(0);
if(cheater) for(int i=0; i<landtypes; i++) landvisited[i] = true;
for(int i=0; i<landtypes; i++)
if(hiitemsMax(treasureType(eLand(i))) >= 25) landvisited[i] = true;
landvisited[laCrossroads] = true;
landvisited[laCrossroads4] = true;
landvisited[laIce] = true;
landvisited[laHunting] = true;
landvisited[laMirrorOld] = true;
landvisited[laPrincessQuest] = cheater || princess::everSaved;
landvisited[laWildWest] = true;
landvisited[laHalloween] = true;
landvisited[laWarpCoast] = true;
landvisited[laGraveyard] = true;
landvisited[laDual] = true;
landvisited[laDocks] |= landvisited[laWarpCoast];
landvisited[laSnakeNest] |= landvisited[laRedRock];
landvisited[laCamelot] |= hiitemsMax(treasureType(laCamelot)) >= 1;
landvisited[laCA] = true;
landvisited[laAsteroids] = true;
dialog::init(XLAT("experiment with geometry"));
// dialog::addSelItem(XLAT("geometry"), XLAT(bitruncnames[int(variation)]), '5');
// dialog::addBreak(50);
generateLandList([] (eLand l) { return land_validity(l).flags & lv::appears_in_geom_exp; });
stable_sort(landlist.begin(), landlist.end(), [] (eLand l1, eLand l2) { return land_validity(l1).quality_level > land_validity(l2).quality_level; });
for(int i=0; i<euperpage; i++) {
if(euperpage * eupage + i >= isize(landlist)) { dialog::addBreak(100); break; }
eLand l = landlist[euperpage * eupage + i];
char ch;
if(i < 26) ch = 'a' + i;
else ch = 'A' + (i-26);
string s = XLAT1(linf[l].name);
if(landvisited[l]) {
dialog::addBoolItem(s, l == specialland, ch);
}
else {
dialog::addSelItem(s, XLAT("(locked)"), ch);
}
dialog::lastItem().color = linf[l].color;
dialog::lastItem().value += validclasses[land_validity(l).quality_level];
dialog::add_action([l] {
if(landvisited[l]) dialog::do_if_confirmed(dual::mayboth([l] {
stop_game_and_switch_mode(tactic::on ? rg::tactic : rg::nothing);
firstland = specialland = l;
if(l == laCanvas || l == laAsteroids)
land_structure = lsSingle;
else if(among(l, laCrossroads, laCrossroads2))
land_structure = lsNiceWalls;
else if(among(l, laCrossroads4))
land_structure = lsNoWalls;
start_game();
popScreen();
}));
});
}
dialog::addItem(XLAT("next page"), '-');
dialog::addBreak(25);
validity_info();
dialog::addBreak(25);
dual::add_choice();
dialog::addBack();
dialog::display();
keyhandler = [] (int sym, int uni) {
dialog::handleNavigation(sym, uni);
if(uni == '-' || uni == PSEUDOKEY_WHEELUP || uni == PSEUDOKEY_WHEELDOWN) {
eupage++;
if(eupage * euperpage >= isize(landlist)) eupage = 0;
}
else if(doexiton(sym, uni)) popScreen();
};
}
#if HDR
struct geometry_filter {
string name;
/** test if the current geometry matches the filter */
function<bool()> test;
};
#endif
EX geometry_filter *current_filter;
bool forced_quotient() { return quotient && !(cgflags & qOPTQ); }
EX geometry_filter gf_hyperbolic = {"hyperbolic", [] { return (arcm::in() || arb::in() || hyperbolic) && !forced_quotient(); }};
EX geometry_filter gf_spherical = {"spherical", [] { return (arcm::in() || arb::in() || sphere) && !forced_quotient(); }};
EX geometry_filter gf_euclidean = {"Euclidean", [] { return (arcm::in() || arb::in() || euclid) && !forced_quotient(); }};
EX geometry_filter gf_other = {"non-isotropic", [] { return prod || nonisotropic; }};
EX geometry_filter gf_regular_2d = {"regular 2D tesselations", [] {
return standard_tiling() && WDIM == 2 && !forced_quotient();
}};
EX geometry_filter gf_regular_3d = {"regular 3D honeycombs", [] {
if(euclid) return geometry == gCubeTiling;
return !bt::in() && !kite::in() && WDIM == 3 && !forced_quotient() && !nonisotropic && !prod;
}};
EX geometry_filter gf_quotient = {"interesting quotient spaces", [] {
return forced_quotient() && !elliptic;
}};
vector<geometry_filter*> available_filters = { &gf_hyperbolic, &gf_spherical, &gf_euclidean, &gf_other, &gf_regular_2d, &gf_regular_3d, &gf_quotient };
void ge_select_filter() {
cmode = sm::SIDE | sm::MAYDARK;
gamescreen(2);
dialog::init(XLAT("geometries"));
char x = 'a';
for(auto f: available_filters) {
if(current_filter)
dialog::addBoolItem(XLAT(f->name), f == current_filter, x++);
else
dialog::addItem(XLAT(f->name), x++);
dialog::add_action([f] { current_filter = f; popScreen(); });
}
dialog::addBack();
dialog::display();
}
void set_default_filter() {
current_filter = hyperbolic ? &gf_hyperbolic : euclid ? &gf_euclidean : sphere ? &gf_spherical : &gf_other;
for(auto f: available_filters) if(f->test()) current_filter = f;
}
void set_or_configure_geometry(eGeometry g) {
if(0) ;
#if CAP_CRYSTAL
else if(g == gCrystal)
pushScreen(crystal::show);
#endif
#if CAP_ARCM
else if(g == gArchimedean)
pushScreen(arcm::show);
#endif
else if(g == gArbitrary)
arb::choose();
else {
if(among(g, gProduct, gRotSpace)) {
if(WDIM == 3 || (g == gRotSpace && euclid)) {
addMessage(XLAT(
g == gRotSpace ?
"Only works with 2D non-Euclidean geometries"
: "Only works with 2D geometries"
));
return;
}
if(g == gRotSpace) {
bool ok = true;
if(arcm::in()) ok = PURE;
else if(bt::in() || kite::in()) ok = false;
else ok = PURE || BITRUNCATED;
if(!ok) {
addMessage(XLAT("Only works with (semi-)regular tilings"));
return;
}
#if CAP_ARCM
if(arcm::in()) {
int steps, single_step;
if(!arcm::current.get_step_values(steps, single_step)) {
addMessage(XLAT("That would have %1/%2 levels", its(steps), its(single_step)));
return;
}
}
#endif
}
}
dual::may_split_or_do([g] { set_geometry(g); });
start_game();
}
}
/** is g2 the same tiling as the current geometry (geometry)? */
bool same_tiling(eGeometry g2) {
if(g2 == gCrystal)
return S3 == 4;
if(g2 == gFieldQuotient && hyperbolic && standard_tiling())
return true;
if(g2 == gFieldQuotient && geometry != gFieldQuotient) {
int ce = 0;
for(auto& ge: fieldpattern::fgeomextras) {
if(ginf[ge.base].tiling_name == ginf[geometry].tiling_name) {
fieldpattern::current_extra = ce;
return true;
}
ce++;
}
}
return ginf[g2].tiling_name == ginf[geometry].tiling_name;
}
void ge_select_tiling() {
cmode = sm::SIDE | sm::MAYDARK;
gamescreen(0);
if(!current_filter) { popScreen(); return; }
dialog::init();
dialog::addItem(XLAT(current_filter->name), 'x');
dialog::add_action_push(ge_select_filter);
vector<eGeometry> geometries;
dialog::addBreak(100);
char letter = 'a';
for(int i=0; i<isize(ginf); i++) {
eGeometry g = eGeometry(i);
if(among(g, gProduct, gRotSpace)) hybrid::configure(g);
bool orig_el = elliptic;
bool on = geometry == g;
bool in_2d = WDIM == 2;
dynamicval<eGeometry> cg(geometry, g);
if(cgflags & qDEPRECATED) continue;
if(arcm::in() && !CAP_ARCM) continue;
if(cryst && !CAP_CRYSTAL) continue;
if(sol && !CAP_SOLV) continue;
if(arb::in() && (ISMOBILE || ISWEB)) continue;
if(WDIM == 3 && MAXMDIM == 3) continue;
if(geometry == gFieldQuotient && !CAP_FIELD) continue;
if(geometry == gFake) continue;
if(!current_filter->test()) continue;
if(orig_el) {
for(int j=0; j<isize(ginf); j++)
if(ginf[j].tiling_name == ginf[i].tiling_name)
geometry = g = eGeometry(j);
}
dialog::addBoolItem(XLAT(
(geometry == gProduct && in_2d) ? XLAT("current geometry x E") :
(geometry == gRotSpace && in_2d) ? XLAT("space of rotations in current geometry") :
ginf[g].menu_displayed_name), on, letter++);
dialog::lastItem().value += validclasses[land_validity(specialland).quality_level];
dialog::add_action([g] { set_or_configure_geometry(g); });
}
dialog::addBreak(100);
dual::add_choice();
dialog::addBack();
dialog::display();
}
EX string current_proj_name() {
bool h = hyperbolic || sn::in();
if(vpconf.model == mdPanini && vpconf.alpha == 1)
return XLAT("stereographic Panini");
else if(vpconf.model != mdDisk)
return models::get_model_name(vpconf.model);
else if(h && vpconf.alpha == 1)
return XLAT("Poincaré model");
else if(h && vpconf.alpha == 0)
return XLAT("Klein-Beltrami model");
else if(h && vpconf.alpha == -1)
return XLAT("inverted Poincaré model");
else if(sphere && vpconf.alpha == 1)
return XLAT("stereographic projection");
else if(sphere && vpconf.alpha == 0)
return XLAT("gnomonic projection");
else if(sphere && vpconf.alpha >= 999)
return XLAT("orthographic projection");
else if(h && vpconf.alpha >= 999)
return XLAT("Gans model");
else
return XLAT("general perspective");
}
EX string dim_name() {
return " (" + its(WDIM) + "D)";
}
#if CAP_THREAD && MAXMDIM >= 4
EX void showQuotientConfig3() {
using namespace fieldpattern;
gamescreen(2);
dialog::init(XLAT("field quotient"));
auto& ds = discoveries[cginf.tiling_name];
if(!ds.discoverer) {
dialog::addItem("start discovery", 's');
dialog::add_action([&ds] { ds.activate(); });
}
else if(ds.is_suspended) {
dialog::addItem("resume discovery", 's');
dialog::add_action([&ds] { ds.activate(); });
}
else {
dialog::addItem("suspend discovery", 's');
dialog::add_action([&ds] { ds.suspend(); });
}
auto& e = ds.experiment;
if(!e.Prime)
dialog::addBreak(100);
else {
string s = its(e.Prime);
if(e.wsquare) s += "²";
dialog::addInfo(s);
}
dialog::addBreak(100);
if(1) {
std::unique_lock<std::mutex> lk(ds.lock);
auto&l = ds.hashes_found;
for(auto& v: l) {
char x = 'a';
string s = XLAT("#%1, cells: %2, p=%3", itsh(v.first), its(get<5>(v.second)), its(get<0>(v.second)) + (get<1>(v.second) ? "²" : ""));
dialog::addItem(s, x++);
dialog::add_action([&v] {
stop_game();
int tmp;
tie(currfp.Prime, currfp.wsquare, currfp.R, currfp.P, currfp.X, tmp) = v.second;
currfp.Field = currfp.wsquare ? currfp.Prime * currfp.Prime : currfp.Prime;
currfp.generate_all3();
currfp.analyze();
start_game();
});
}
}
dialog::addBreak(100);
dialog::addBack();
dialog::display();
}
#endif
EX string geometry_name() {
switch(ginf[geometry].cclass) {
case gcHyperbolic:
return XLAT("hyperbolic") + dim_name();
case gcEuclid:
if(cgflags & qAFFINE)
return XLAT("affine") + dim_name();
return XLAT("flat") + dim_name();
case gcSphere:
return XLAT("spherical") + dim_name();
case gcSolNIH:
#if CAP_SOLV
switch(sn::geom()) {
case gSol:
return XLAT("Sol");
case gNIH:
return XLAT("hyperbolic (3:2)");
case gSolN:
return XLAT("Sol (3:2)");
default:
return "unknown";
}
#else
return XLAT("Sol");
#endif
case gcNil:
return XLAT("Nil");
case gcSL2:
return XLAT("~SL(2,R)~");
case gcProduct:
return XLAT("%1 x E", PIU(geometry_name()));
}
return "?";
}
EX void select_quotient_screen() {
cmode = sm::SIDE | sm::MAYDARK;
gamescreen(0);
dialog::init(XLAT("quotient spaces in ") + ginf[geometry].tiling_name);
char key = 'a';
for(int i=0; i<isize(ginf); i++) {
auto g = eGeometry(i);
if(ginf[g].flags & qDEPRECATED) continue;
if(same_tiling(g)) {
dialog::addBoolItem(
(ginf[g].flags & qANYQ) ?
XLAT(ginf[g].menu_displayed_name) :
"no quotient",
g == geometry, key++);
dialog::add_action([g] {
if(g == gFieldQuotient && WDIM == 3) {
if(geometry != gFieldQuotient) {
stop_game();
fieldpattern::field_from_current();
set_geometry(gFieldQuotient);
for(int p=2;; p++) { currfp.Prime = p; currfp.force_hash = 0; if(!currfp.solve()) break; }
println(hlog, "set prime = ", currfp.Prime);
start_game();
}
#if CAP_THREAD && MAXMDIM >= 4
pushScreen(showQuotientConfig3);
#endif
}
else if(g == gFieldQuotient)
pushScreen(showQuotientConfig);
#if CAP_CRYSTAL
else if(g == gCrystal)
pushScreen(crystal::show);
#endif
else {
dual::may_split_or_do([g] { set_geometry(g); });
start_game();
}
});
}
}
dialog::addBack();
dialog::display();
}
EX void select_quotient() {
if(euclid && !kite::in() && !arcm::in()) {
euc::prepare_torus3();
pushScreen(euc::show_torus3);
}
else if(nil) {
nilv::prepare_niltorus3(),
pushScreen(nilv::show_niltorus3);
}
#if CAP_SOLV
else if(asonov::in()) {
asonov::prepare_config();
pushScreen(asonov::show_config);
}
#endif
else if(prod)
pushScreen(product::show_config);
else if(rotspace)
hybrid::configure_period();
else {
vector<eGeometry> choices;
for(int i=0; i<isize(ginf); i++) if(same_tiling(eGeometry(i))) choices.push_back(eGeometry(i));
println(hlog, "choices = ", choices);
if(isize(choices) > 1)
pushScreen(select_quotient_screen);
/* else if(isize(choices) > 1) {
set_geometry(choices[choices[0] == geometry ? 1 : 0]);
start_game();
} */
else
addMessage("No quotient spaces available in the current tiling.");
}
}
EX string full_geometry_name() {
string qstring = ginf[geometry].quotient_name;
bool variable =
!(prod || hybri || bt::in() || WDIM == 3 || kite::in() || arb::in());
string fgname = XLAT(ginf[geometry].tiling_name);
if(qstring != "none") fgname += " " + XLAT(qstring);
if(variable) fgname = gp::operation_name() + " " + fgname;
return fgname;
}
void action_change_variation() {
if(0) ;
#if CAP_ARCM
else if(arcm::in()) arcm::next_variation();
#endif
else if(euc::in(2,4) || !CAP_GP) dialog::do_if_confirmed([] {
set_variation(PURE ? eVariation::bitruncated : eVariation::pure);
start_game();
});
#if CAP_GP
else // if(S3 == 3)
gp::configure();
#endif
}
EX void menuitem_change_variation(char key) {
dialog::addSelItem(XLAT("variations"), gp::operation_name(), key);
dialog::add_action(action_change_variation);
}
EX void menuitem_change_geometry(char key) {
dialog::addSelItem(XLAT("geometry/topology/tiling"), full_geometry_name(), key);
dialog::add_action_push(current_filter ? ge_select_tiling : ge_select_filter);
}
EX void menuitem_projection(char key) {
dialog::addSelItem(XLAT("projection"), current_proj_name(), key);
dialog::add_action_push(models::model_menu);
}
EX void menuitem_binary_width(char key) {
dialog::addSelItem(XLAT("binary tiling width"), fts(vid.binary_width), key);
dialog::add_action([] {
dialog::editNumber(vid.binary_width, 0, 2, 0.1, 1, XLAT("binary tiling width"), "");
dialog::reaction = [] () {
#if CAP_TEXTURE
texture::config.remap();
#endif
#if CAP_SOLV
if(asonov::in()) asonov::prepare();
#endif
};
});
}
EX void menuitem_nilwidth(char key) {
dialog::addSelItem(XLAT("nil width"), fts(nilv::nilwidth), key);
dialog::add_action([] {
dialog::editNumber(nilv::nilwidth, 0.01, 2, 0.1, 1, XLAT("Nil width"), "");
dialog::reaction = ray::reset_raycaster;
dialog::bound_low(0.01);
});
}
EX void edit_stretch() {
stretch::mstretch = false;
ray::reset_raycaster();
dialog::editNumber(stretch::factor, -1, 9, 0.1, 0, XLAT("stretched geometry"),
XLAT(
"Stretch the metric along the fibers. This can currently be done in rotation spaces and in 8-cell, 24-cell and 120-cell. "
"Value of 0 means not stretched, -1 means S2xE or H2xE (works only in the limit). (Must be > -1)"
)
);
dialog::reaction = [] { if(abs(stretch::factor+1) < 1e-3) stretch::factor = -.9; ray::reset_raycaster(); };
}
EX void showEuclideanMenu() {
// for(int i=2; i<lt; i++) landvisited[i] = true;
cmode = sm::SIDE | sm::MAYDARK;
gamescreen(0);
dialog::init(XLAT("experiment with geometry"));
dialog::addSelItem(XLAT("geometry"), geometry_name(), 'd');
dialog::add_action([] { pushScreen(ge_select_tiling); pushScreen(ge_select_filter); });
dialog::addSelItem(XLAT("basic tiling"), XLAT(ginf[geometry].tiling_name), 't');
dialog::add_action([] {
if(!current_filter || !current_filter->test()) set_default_filter();
pushScreen(ge_select_tiling);
});
int ts = ginf[geometry].sides;
int tv = ginf[geometry].vertex;
int nom = (BITRUNCATED ? tv+ts : tv) * 4;
int denom = (2*ts + 2*tv - ts * tv);
#if CAP_GP
if(GOLDBERG || INVERSE) {
ld area = PIU(cgi.gpdata->area);
if(GOLDBERG || WARPED) {
nom = 2 * (2*tv + (S3-2) * ts * (area-1));
}
else if(UNRECTIFIED) {
if((gp::param.first + gp::param.second) % 2 == 0)
nom = ts * 2 * area;
else
nom = (2*tv + (S3-2) * ts * (area-1));
}
else if(UNTRUNCATED) {
if((gp::param.first - gp::param.second) % 3 == 0) {
nom = ts * 4 * area;
denom *= 3;
}
else {
nom = 2 * (2*tv + (S3-2) * ts * (area-1));
denom *= 3;
}
}
}
#endif
int worldsize;
int euler = 0;
if(euclid) euler = 0;
else if(sphere && nonorientable) euler = 1;
else if(sphere) euler = 2;
else if(!bounded) euler = -2;
else if(WDIM == 3) euler = 0;
else switch(geometry) {
case gFieldQuotient:
worldsize = isize(currentmap->allcells());
euler = 2 * worldsize * denom / nom;
break;
case gMinimal:
euler = -1;
break;
case gZebraQuotient:
case gBolza:
euler = -2;
break;
case gKleinQuartic:
case gSchmutzM2:
case gBolza2:
euler = -4;
break;
case gSchmutzM3:
case gBring:
euler = -6;
break;
case gMacbeath:
euler = -12;
break;
default:
worldsize = isize(currentmap->allcells());
println(hlog, "warning: Euler characteristics unknown, worldsize = ", worldsize);
euler = 2 * worldsize * denom / nom;
break;
}
nom *= euler;
denom *= 2;
if(hybri) nom *= hybrid::csteps, denom *= cgi.single_step;
int g = gcd(nom, denom);
if(g) {
nom /= g;
denom /= g;
}
if(euclid && bounded) {
worldsize = euc::eu.det;
if(BITRUNCATED) worldsize *= (a4 ? 2 : 3);
if(GOLDBERG) worldsize *= cgi.gpdata->area;
#if CAP_IRR
if(IRREGULAR) worldsize *= isize(irr::cells) / isize(irr::cells_of_heptagon);
#endif
}
else
worldsize = denom ? nom / denom : 0;
if(euler < 0 && !bounded)
worldsize = -worldsize;
string spf = its(ts);
if(0) ;
#if CAP_ARCM
else if(arcm::in()) {
spf = "";
for(int i: arcm::current.faces) {
if(spf != "") spf += ",";
spf += its(i);
}
if(BITRUNCATED) spf = "[" + spf + "]," + its(arcm::current.N * 2) + "," + its(arcm::current.N * 2);
if(DUAL) spf = its(arcm::current.N) + "^[" + spf + "]";
}
#endif
#if CAP_BT
else if(bt::in()) switch(geometry) {
case gBinaryTiling: spf = "6,[6,7],7"; break;
case gBinary4: spf = "5,5,5[,5]"; break;
case gTernary: spf = "6,6,6[,6]"; break;
default: spf = "?"; break;
}
#endif
else if(BITRUNCATED)
spf = spf + "," + its(S6) + "," + its(S6);
#if CAP_IRR
else if(IRREGULAR && irr::bitruncations_performed)
spf = "[4..8],6,6";
else if(IRREGULAR)
spf = "[4..8]^3";
#endif
#if CAP_GP
else if(UNRECTIFIED || UNTRUNCATED) {
if(UNRECTIFIED && (gp::param.first-gp::param.second) % 2 != 0)
spf = "(?)";
else if(UNTRUNCATED && (gp::param.first-gp::param.second) % 3 != 0)
spf = "(?)";
else {
string each = UNRECTIFIED ? "4" : "3";
int first = UNRECTIFIED ? 4 : 6;
int second = S7;
if(gp::param == gp::loc(1, 1))
first = second;
else if(second < first)
swap(first, second);
spf = each;
for(int z=1; z<first; z++) spf += "," + each;
if(first != second) {
spf += "[";
for(int z=first; z<second; z++) spf += "," + each;
spf += "]";
}
}
}
else if(GOLDBERG && S3 == 4 && gp::param == gp::loc(1, 1))
spf = spf + ",4," + spf + ",4";
else if(GOLDBERG && S3 == 4 && gp::param == gp::loc(2, 0))
spf = spf + ",4,4,4";
else if(GOLDBERG && S3 == 4)
spf = "[" + spf + ",4],4,4,4";
else if(WARPED && S3 == 3 && gp::param == gp::loc(1,1))
spf = spf + ",3,3";
else if(WARPED && S3 == 3)
spf = "[" + spf + ",6],3,3";
else if(GOLDBERG && S3 == 3)
spf = "[" + spf + ",6],6,6";
#endif
else {
string spf0 = spf;
for(int z=1; z<S3; z++) spf = spf + "," + spf0;
}
string qstring = ginf[geometry].quotient_name;
if(qstring == "none")
dialog::addBoolItem(XLAT("quotient space"), false, 'q');
else
dialog::addSelItem(XLAT("quotient space"), XLAT(qstring), 'q');
dialog::add_action(select_quotient);
#if CAP_ARCM
if(arcm::in()) {
dialog::addItem(XLAT("advanced parameters"), '4');
dialog::add_action_push(arcm::show);
}
#endif
#if CAP_CRYSTAL
if(cryst) {
dialog::addItem(XLAT("advanced parameters"), '4');
dialog::add_action_push(crystal::show);
}
#endif
if(fake::available()) {
dialog::addItem(XLAT("fake curvature"), '4');
dialog::add_action([] {
if(fake::in()) fake::configure();
else dialog::cheat_if_confirmed(
fake::configure
);
});
}
if(arb::in() && !arb::current.sliders.empty()) {
dialog::addItem(XLAT("tessellation sliders"), '4');
dialog::add_action_push(arb::set_sliders);
}
#if CAP_IRR
if(hyperbolic && IRREGULAR) {
nom = isize(irr::cells);
// both Klein Quartic and Bolza2 are double the Zebra quotiennt
denom = -2;
if(!quotient) worldsize = nom / denom;
}
#endif
#if MAXMDIM >= 4
if(cgflags & qULTRA) {
dialog::addBoolItem(XLAT("truncate ultra-vertices with mirrors"), reg3::ultra_mirror_on, 'Z');
dialog::add_action([] {
reg3::ultra_mirror_on = !reg3::ultra_mirror_on;
ray::reset_raycaster();
});
}
#endif
if(prod) {
dialog::addSelItem(XLAT("Z-level height factor"), fts(vid.plevel_factor), 'Z');
dialog::add_action([] {
dialog::editNumber(vid.plevel_factor, 0, 2, 0.1, 0.7, XLAT("Z-level height factor"), "");
});
}
else if(hybri) {
dialog::addSelItem(XLAT("number of levels"), its(hybrid::csteps / cgi.single_step), 'L');
dialog::add_action(hybrid::configure_period);
}
else if(bt::in()) {
menuitem_binary_width('v');
add_edit_wall_quality('W');
}
else if(nil) {
menuitem_nilwidth('v');
}
else if(WDIM == 3 || kite::in() || arb::in()) dialog::addBreak(100);
else
menuitem_change_variation('v');
if(in_s2xe()) {
dialog::addSelItem(XLAT("precision of S2xE rings"), its(s2xe::qrings), '5');
dialog::add_action([] {
dialog::editNumber(s2xe::qrings, 1, 256, 4, 32, XLAT("precision of S2xE rings"),
XLAT(
"In S2xE, objects at spherical distances which are multiples of π will look like "
"rings, and objects close to these will look like crescents. "
"This setting controls the quality of rendering these rings and crescents.")
);
dialog::bound_low(1);
dialog::bound_up(256);
});
}
#if MAXMDIM >= 4
if(hybri) {
auto r = rots::underlying_scale;
dialog::addSelItem(XLAT("view the underlying geometry"), r > 0 ? fts(r)+"x" : ONOFF(false), '6');
dialog::add_action([] {
dialog::editNumber(rots::underlying_scale, 0, 1, 0.05, 0.25, XLAT("view the underlying geometry"),
XLAT(
geometry == gRotSpace ? "The space you are currently in is the space of rotations of the underlying hyperbolic or spherical geometry. "
: "You are currently in a product space.") +
XLAT(
"This option lets you see the underlying space. Lands and some walls (e.g. in the Graveyard) are based on "
"the respective features in the underlying world, but details such as monsters or items are ignored."
)
);
dialog::bound_low(0);
dialog::bound_up(1);
dialog::extra_options = [] () { rots::draw_underlying(true); };
});
}
#endif
if(stretch::applicable()) {
dialog::addSelItem(XLAT("stretched geometry"), fts(stretch::factor), 'S');
dialog::add_action(edit_stretch);
}
dialog::addBreak(100);
menuitem_land_structure('l');
if(specialland == laMinefield && bounded) {
dialog::addSelItem(XLAT("number of mines"), its(bounded_mine_quantity), 'm');
dialog::add_action([] {
dialog::editNumber(bounded_mine_quantity, 0, bounded_mine_max, 1, (bounded_mine_max+5)/10,
XLAT("number of mines"), "");
dialog::reaction = [] {
if(bounded_mine_quantity < 0) bounded_mine_quantity = 0;
if(bounded_mine_quantity > bounded_mine_max) bounded_mine_quantity = bounded_mine_max;
};
dialog::reaction_final = [] {
bounded_mine_percentage = bounded_mine_quantity * 1. / bounded_mine_max;
stop_game();
start_game();
};
});
}
if(geometry_has_alt_mine_rule()) {
dialog::addSelItem(XLAT("adjacency rule"), XLAT(mine_adjacency_rule ? "vertex" : WDIM == 3 ? "face" : "edge"), 'M');
dialog::add_action([] {
stop_game();
mine_adjacency_rule = !mine_adjacency_rule;
start_game();
addMessage(XLAT("Note: adjacency rule affects environmental effects, but not movement."));
});
}
dialog::addBoolItem(XLAT("pattern"), specialland == laCanvas, 'p');
if(specialland == laCanvas) dialog::lastItem().value = patterns::whichCanvas;
dialog::add_action_push(patterns::showPrePattern);
validity_info();
if(WDIM == 3) {
dialog::addItem(XLAT("3D configuration"), '9');
dialog::add_action_push(show3D);
}
menuitem_projection('1');
if(nonisotropic && !sl2)
dialog::addBoolItem_action(XLAT("geodesic movement in Sol/Nil"), nisot::geodesic_movement, 'G');
#if CAP_CRYSTAL && MAXMDIM >= 4
crystal::add_crystal_transform('x');
#endif
dialog::addBreak(50);
#if CAP_SHOT
dialog::addItem(XLAT("take screenshot"), 's');
dialog::add_action_push(shot::menu);
#endif
dialog::addHelp();
dialog::addBack();
dialog::addBreak(150);
dialog::addTitle(XLAT("info about: %1", full_geometry_name()), 0xFFFFFF, 150);
if(WDIM == 2 && !arb::in() && !kite::in()) dialog::addSelItem(XLAT("faces per vertex"), spf, 0);
if(arb::in() && arb::current.comment != "") {
dialog::addBreak(100);
dialog::addHelp(arb::current.comment);
}
dialog::addSelItem(XLAT("size of the world"),
#if CAP_BT
bt::in() ? fts(8 * M_PI * sqrt(2) * log(2) / pow(vid.binary_width, WDIM-1), 4) + " exp(∞)" :
#endif
#if CAP_ARCM
arcm::in() && (WDIM == 2) ? arcm::current.world_size() :
(arcm::in() && sphere) ? its(isize(currentmap->allcells())) :
#endif
#if CAP_CRYSTAL
cryst ? "∞^" + its(ts/2) :
#endif
WDIM == 3 && bounded ? its(isize(currentmap->allcells())) :
WDIM == 3 && euclid ? "" :
worldsize < 0 ? (nom%denom ? its(nom)+"/"+its(denom) : its(-worldsize)) + " exp(∞)":
(euclid && quotient && !bounded) ? "" :
worldsize == 0 ? "∞²" :
its(worldsize),
'3');
if(WDIM == 2 || reg3::in_rule()) dialog::add_action([] {
if(!viewdists) { enable_viewdists(); pushScreen(viewdist_configure_dialog); }
else if(viewdists) viewdists = false;
});
if(bounded) {
if(WDIM == 3) euler = 0;
dialog::addSelItem(XLAT("Euler characteristics"), its(euler), 0);
if(WDIM == 3) ;
else if(nonorientable)
dialog::addSelItem(XLAT("demigenus"), its(2-euler), 0);
else
dialog::addSelItem(XLAT("genus"), its((2-euler)/2), 0);
}
else dialog::addBreak(200);
dialog::display();
}
EX void runGeometryExperiments() {
if(!geometry && specialland == laIce)
specialland = getLandForList(cwt.at);
pushScreen(showEuclideanMenu);
}
#if CAP_COMMANDLINE
EX eGeometry readGeo(const string& ss) {
for(int i=0; i<isize(ginf); i++) if(ginf[i].shortname == ss) return eGeometry(i);
bool numeric = true;
for(char c: ss) if(c < '0' || c > '9') numeric = false;
if(numeric) return eGeometry(atoi(ss.c_str()));
for(int i=0; i<isize(ginf); i++) if(appears(ginf[i].menu_displayed_name, ss)) {
return eGeometry(i);
break;
}
return gNormal;
}
EX void field_quotient_3d(int p, unsigned hash) {
check_cgi();
cgi.require_basics();
stop_game_and_switch_mode(rg::nothing);
fieldpattern::field_from_current();
set_geometry(gFieldQuotient);
for(;; p++) {
println(hlog, "trying p = ", p);
currfp.Prime = p; currfp.force_hash = hash; if(!currfp.solve()) break;
}
println(hlog, "set prime = ", currfp.Prime);
}
EX void field_quotient_2d(int group, int id, int triplet) {
using namespace fieldpattern;
current_extra = group;
auto& gxcur = fgeomextras[current_extra];
while(id >= isize(gxcur.primes)) nextPrime(gxcur);
fgeomextras[current_extra].current_prime_id = id;
if(triplet != -1)
triplet_id = triplet;
enableFieldChange();
set_geometry(gFieldQuotient);
}
int read_geom_args() {
using namespace arg;
if(0) ;
#if CAP_FIELD
else if(argis("-qpar")) {
int p;
shift(); sscanf(argcs(), "%d,%d,%d",
&p, &quotientspace::rvadd, &quotientspace::rvdir
);
autocheat = true;
currfp.init(p);
}
else if(argis("-qpar2")) {
stop_game_and_switch_mode(rg::nothing);
int a, b;
shift(); sscanf(argcs(), "%d,%d", &a, &b);
field_quotient_2d(a, b, -1);
}
else if(argis("-triplet")) {
stop_game();
shift(); fieldpattern::triplet_id = argi();
fieldpattern::enableFieldChange();
}
else if(argis("-to-fq")) {
int p = 2;
shift();
if(args() == "p") {
shift(); p = argi();
shift();
}
unsigned hash = arghex();
field_quotient_3d(p, hash);
}
else if(argis("-cs")) {
shift(); cheat();
fieldpattern::matrix M = currfp.strtomatrix(args());
fieldpattern::subpathid = currfp.matcode[M];
fieldpattern::subpathorder = currfp.order(M);
}
else if(argis("-fwrite")) {
shstream hs;
hwrite_fpattern(hs, currfp);
println(hlog, "current fieldpattern: ", as_cstring(hs.s));
}
else if(argis("-csp")) {
cheat();
currfp.findsubpath();
}
#endif
else if(argis("-mineadj")) {
shift(); mine_adjacency_rule = argi();
}
TOGGLE('7', PURE, set_variation(PURE ? eVariation::bitruncated : eVariation::pure))
else if(argis("-geo")) {
PHASEFROM(2);
shift();
set_geometry(readGeo(args()));
}
#if CAP_GP
else if(argis("-gp")) {
PHASEFROM(2);
shift(); gp::param.first = argi();
shift(); gp::param.second = argi();
set_variation(eVariation::goldberg);
}
else if(argis("-unrectified")) {
PHASEFROM(2);
set_variation(eVariation::unrectified);
}
else if(argis("-untruncated")) {
PHASEFROM(2);
set_variation(eVariation::untruncated);
}
else if(argis("-warped")) {
PHASEFROM(2);
set_variation(eVariation::warped);
}
#endif
#if CAP_FIELD
else if(argis("-fi")) {
fieldpattern::info();
exit(0);
}
else if(argis("-fi-at")) {
geometry = gNormal;
shift(); dynamicval<int> s7(S7, argi());
shift(); dynamicval<int> s3(S3, argi());
fieldpattern::info();
exit(0);
}
else if(argis("-fi-geo")) {
fieldpattern::info();
exit(0);
}
else if(argis("-qs")) {
cheat();
shift(); currfp.qpaths.push_back(args());
}
#if MAXMDIM >= 4
else if(argis("-truncate-ultra")) {
shift(); reg3::ultra_mirror_on = argi();
}
#endif
else if(argis("-d:quotient"))
launch_dialog(showQuotientConfig);
#endif
else if(argis("-d:geom"))
launch_dialog(showEuclideanMenu);
else return 1;
return 0;
}
auto ah_geom = addHook(hooks_args, 0, read_geom_args);
#endif
}