The hyperconway.py script doesn't work very well on macOS (buttons overlap and right-clicking isn't supported) so I got Claude to convert the Python code to Lua so it runs in Glu (or Glui on iPad):
I also got Claude to implement click-and-drag so you can move a tile to anywhere on the disk. Here is the Lua code in case Entity Valkyrie 2 wants to back port that capability into the Python version:
Code: Select all
--[[
Life-like cellular automata on a hyperbolic tiling of heptagons and hexagons.
Converted from HyperConway 1.9, a Python/tkinter script by Valky River.
Code: https://github.com/ValkyRiver/hyperconway
Examples: https://conwaylife.com/forums/viewtopic.php?t=5300
Lua version for Glu by Claude Pro and Andrew Trevorrow (andrew@trevorrow.com).
--]]
local glu = glu()
local cv = canvas()
-- require "gplus.strict"
local gp = require "gplus"
local cp = require "cplus"
local iOS = glu.os() == "iOS"
if iOS then
glu.setoption("showexit",0) -- hide the EXIT button
end
local sin = math.sin
local cos = math.cos
local pi = math.pi
local floor = math.floor
local version = "1.9"
local viewwd, viewht = 1280, 726 -- initial viewport size (for a scale of 660)
local minwd, minht = 970, 550 -- minimum viewport size
local scale = 660 -- all positions and sizes are relative to this
-- colors:
local bgcolor = {236, 236, 236, 255}
local oncolor = {0, 204, 255, 255}
local offcolor = {255, 255, 255, 255}
local onheptcolor = {0, 187, 238, 255}
local offheptcolor = {238, 238, 238, 255}
local highlightcolor = {255, 204, 0, 255}
local highlightheptcolor = {238, 187, 0, 255}
local historycolor = {170, 255, 204, 255}
local historyheptcolor = {153, 238, 187, 255}
local bordercolor = {0, 0, 0, 255}
local textcolor = {0, 0, 0, 255}
-- state:
local universe = {[0] = {}} -- universe[g] is the set of live cells in generation g
local lastgen = 0 -- last generation stored in universe
local gen = 0 -- current generation
local rule = {} -- set of transitions like "B3o" and "S2m"
local density = 0.5 -- density for random fill
local radius = 2 -- radius for random fill
local history = false -- show cells that were alive in earlier generations?
local playing = false -- generating?
local speed = 4 -- generations per second
local nextstep = 0 -- time (in millisecs) of next generation while playing
local polys = {} -- polygons drawn by DrawUniverse (for hit testing)
local nbrcache = {} -- cached results of Neighbors
local heptcache = {} -- cached results of HeptInfo
local panelx = 0 -- left edge of rulespanel clip
-- controls:
local resetbutton, prevbutton, nextbutton, playbutton, exitbutton
local speeddown, speedup
local historybutton, periodbutton, exportbutton, importbutton
local originbutton, randombutton, clearbutton
local radiusdown, radiusup, densitydown, densityup
local rulebox = {} -- check boxes for each transition
local speedlist = {0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6, 7, 8, 9,
10, 11, 12, 13, 14, 15, 16, 18, 20, 22, 24, 26, 28, 30, 32}
--------------------------------------------------------------------------------
-- CELL FORMAT
-- HEPTAGONS
-- X is the origin heptagon
-- X0, X1, X2, X3, X4, X5, X6 are the heptagons corner from the origin
-- then, the heptagons branch tree - all branches split into 3 (L M R), except if it ends in R
-- HEXAGONS
-- a refers to the hexagon on the left side of the edge between two heptagons
-- if the edge is R, then there is also a hexagon on the right: b
-- the universe only stores live cells
-- neighborhoods (o = ortho, m = meta, p = para, g = gyro)
local hept_formations = {
{"0000000", "0o"},
{"1000000", "1o"},
{"1100000", "2o"},
{"1010000", "2m"},
{"1001000", "2p"},
{"1110000", "3o"},
{"1101000", "3m"},
{"1100100", "3p"},
{"1010100", "3g"},
{"1111000", "4o"},
{"1110100", "4m"},
{"1101100", "4p"},
{"1101010", "4g"},
{"1111100", "5o"},
{"1111010", "5m"},
{"1110110", "5p"},
{"1111110", "6o"},
{"1111111", "7o"},
}
local hex_formations = {
{"000000", "0a"},
{"100000", "1x"},
{"010000", "1h"},
{"110000", "2a"},
{"101000", "2x"},
{"010100", "2h"},
{"100100", "2c"},
{"111000", "3a"},
{"110001", "3b"},
{"101100", "3c"},
{"110100", "3d"},
{"101010", "3x"},
{"010101", "3h"},
{"111100", "4a"},
{"110101", "4x"},
{"111010", "4h"},
{"110110", "4c"},
{"111101", "5x"},
{"111110", "5h"},
{"111111", "6a"},
}
-- map neighborhood patterns to formation names
local hept_map = {}
for _, f in ipairs(hept_formations) do hept_map[f[1]] = f[2] end
local hex_map = {}
for _, f in ipairs(hex_formations) do hex_map[f[1]] = f[2] end
-- CELL NAMES
-- the cells within distance 7 of the origin heptagon in one of its 7 sectors
-- (prefix each name with "X0".."X6" to get the actual cells)
local cellnames = {
-- distance 1
"a",
-- distance 2
"",
"Rb",
-- distance 3
"La",
"Ma",
"Ra",
"R",
-- distance 4
"L",
"LRb",
"M",
"MRb",
"RLa",
"RRa",
"RRb",
-- distance 5
"LLa",
"LMa",
"LRa",
"LR",
"MLa",
"MMa",
"MRa",
"MR",
"MRRb",
"RL",
"RLRb",
"RR",
-- distance 6
"LL",
"LLRb",
"LM",
"LMRb",
"LRLa",
"LRRa",
"LRRb",
"ML",
"MLRb",
"MM",
"MMRb",
"MRLa",
"MRRa",
"MRR",
"RLLa",
"RLMa",
"RLRa",
"RLR",
"RRLa",
"RRRa",
"RRRb",
-- distance 7
"LLLa",
"LLMa",
"LLRa",
"LLR",
"LMLa",
"LMMa",
"LMRa",
"LMR",
"LMRRb",
"LRL",
"LRLRb",
"LRR",
"MLLa",
"MLMa",
"MLRa",
"MLR",
"MMLa",
"MMMa",
"MMRa",
"MMR",
"MMRRb",
"MRL",
"MRLRb",
"MRRLa",
"MRRRa",
"MRRRb",
"RLL",
"RLLRb",
"RLM",
"RLMRb",
"RLRLa",
"RLRRa",
"RLRRb",
"RRL",
"RRLRb",
"RRR",
}
local numcells = {0, 1, 3, 7, 14, 26, 47, 83} -- number of cellnames entries within each distance
-- these points are only used by DrawFormation to draw the neighborhood diagrams
-- (the 1st number is distance from center, the 2nd is angle in units of pi/7,
-- except for a8..d9 where it's in units of pi/3)
local pt = {
a0 = {0.187, 0.000},
a1 = {0.342, 0.000},
b1 = {0.431, 0.648},
a8 = {0.163, 0.500},
a9 = {0.457, 0.000},
b9 = {0.421, 0.283},
c9 = {0.319, 0.534},
d9 = {0.409, 0.836},
}
--------------------------------------------------------------------------------
-- EXACT GEOMETRY
-- the tiling is drawn in the Poincare disk model of the hyperbolic plane,
-- where points are complex numbers x+iy inside the unit circle
local function sinh(x) return (math.exp(x) - math.exp(-x))/2 end
local function tanh(x) local e = math.exp(2*x); return (e-1)/(e+1) end
local function asinh(x) return math.log(x + math.sqrt(x*x+1)) end
local function acosh(x) return math.log(x + math.sqrt(x*x-1)) end
-- the heptagon centers form a {3,7} tiling of equilateral triangles with angles 2pi/7,
-- and each heptagon's vertices lie on the lines joining its center to its 7 neighbors
local hepttheta = 2*pi/7
local hdist = acosh(cos(hepttheta) / (1 - cos(hepttheta))) -- distance between adjacent centers
-- find the heptagon circumradius that makes a heptagon's edges the same length as the
-- hexagon-hexagon edges (the middle parts of the lines joining adjacent centers)
local heptradius
do
local lo, hi = 0, hdist/2
for _ = 1, 60 do
local r = (lo + hi)/2
if 2*asinh(sinh(r)*sin(pi/7)) > hdist - 2*r then hi = r else lo = r end
end
heptradius = (lo + hi)/2
end
-- a point at hyperbolic distance d from the disk's center is tanh(d/2) from the center
local nbrdist = tanh(hdist/2) -- Poincare distance from a center to an adjacent center
local vertdist = tanh(heptradius/2) -- Poincare distance from a center to its vertices
local maxdist = 4.0 -- only show heptagons within this distance of the disk's center
-- the view is defined by a heptagon (the anchor) and its position and orientation:
local anchor = "X" -- the heptagon closest to the disk's center
local anchorx, anchory = 0, 0 -- position of the anchor's center
local anchora = 0 -- angle from the anchor to its 1st adjacent heptagon
local anchors = 1 -- 1 if its adjacent heptagons go counterclockwise, else -1
local frames = {} -- frames[h] has the position and orientation of heptagon h
local shown = {} -- the heptagons within maxdist (set by BuildTiling)
local tilingchanged = true -- does BuildTiling need to be called?
-- the original layout has angle 0 pointing up and angles increasing clockwise
--------------------------------------------------------------------------------
function Count(set)
local n = 0
for _ in pairs(set) do n = n + 1 end
return n
end
--------------------------------------------------------------------------------
function SortedCells(set)
local cells = {}
for cell in pairs(set) do cells[#cells+1] = cell end
table.sort(cells)
return cells
end
--------------------------------------------------------------------------------
function Shape(cell)
-- return the number of sides of the given cell
local last = cell:sub(-1)
if last == "a" or last == "b" then
return 6
else
return 7
end
end
--------------------------------------------------------------------------------
-- some helpers for the code below (which mimics the Python string indexing)
local function last(s) return s:sub(-1) end
local function last2(s) return s:sub(-2,-2) end -- s[-2] in Python
local function last3(s) return s:sub(-3,-3) end -- s[-3] in Python
local function chop(s, n) return s:sub(1,-n-1) end -- s[0:-n] in Python
local function digit(s) return tonumber(s:sub(2,2)) end -- int(s[1]) in Python
local function isdigit(ch) return ch:find("^[0-6]$") ~= nil end
local rep = string.rep
--------------------------------------------------------------------------------
function StripChar(s, ch)
-- remove any trailing ch chars from s and return the result and how many were removed
local n = 0
while last(s) == ch do
s = chop(s, 1)
n = n + 1
end
return s, n
end
--------------------------------------------------------------------------------
function OppRight(cell2, n)
-- find the opposite heptagon for a cell (stripped of trailing R's)
if isdigit(last(cell2)) then
return "X"..((digit(cell2)+1)%7)..rep("L", n)
elseif last(cell2) == "L" then
if last2(cell2) == "R" then
return chop(cell2,1).."R"..rep("L", n)
else
return chop(cell2,1).."M"..rep("L", n)
end
elseif last(cell2) == "M" then
return chop(cell2,1).."R"..rep("L", n)
end
return ""
end
--------------------------------------------------------------------------------
function OppLeft(cell2, n)
-- find the opposite heptagon for a cell (stripped of trailing L's)
if isdigit(last(cell2)) then
return "X"..((digit(cell2)-1)%7)..rep("R", n)
elseif last(cell2) == "M" then
return chop(cell2,1).."L"..rep("R", n)
elseif last(cell2) == "R" then
if last2(cell2) == "R" then
return chop(cell2,1).."L"..rep("R", n)
else
return chop(cell2,1).."M"..rep("R", n)
end
end
return ""
end
--------------------------------------------------------------------------------
function FindNeighbors(cell)
local c1 = chop(cell,1)
local c2 = chop(cell,2)
-- HEXAGON CASE (always returns hex-hept-hex-hept-hex-hept)
if last(cell) == "b" then
local cell2, recurse = StripChar(c1, "R")
local opp = OppRight(cell2, recurse-1)
return {c1.."a", c1, opp.."La", opp, opp.."a", c2}
elseif last(cell) == "a" and last2(cell) == "R" then
if last3(cell) == "R" then
return {c2.."La", c2.."L", c2.."LRb", c1, c1.."b", c2}
else
return {c2.."Ma", c2.."M", c2.."MRb", c1, c1.."b", c2}
end
elseif last(cell) == "a" and last2(cell) == "M" then
return {c2.."La", c2.."L", c2.."LRb", c1, c2.."Ra", c2}
elseif last(cell) == "a" and last2(cell) == "L" then
local cell2, recurse = StripChar(c1, "L")
local opp = OppLeft(cell2, recurse)
if last3(cell) == "R" then
return {opp.."b", opp, opp.."Rb", c1, c2.."Ra", c2}
else
return {opp.."b", opp, opp.."Rb", c1, c2.."Ma", c2}
end
elseif last(cell) == "a" and isdigit(last2(cell)) then
local d = digit(cell)
local prev = "X"..((d-1)%7)
return {prev.."a", prev, prev.."Rb", c1, "X"..((d+1)%7).."a", "X"}
-- HEPTAGON CASE
elseif last(cell) == "R" then
local cell2, recurse = StripChar(cell, "R")
local opp = OppRight(cell2, recurse)
if last2(cell) == "R" then
return {cell.."La", cell.."Ra", cell.."Rb", opp.."a", cell.."b", cell.."a", c1.."LRb"}
else
return {cell.."La", cell.."Ra", cell.."Rb", opp.."a", cell.."b", cell.."a", c1.."MRb"}
end
elseif last(cell) == "M" then
return {cell.."La", cell.."Ma", cell.."Ra", cell.."Rb", c1.."Ra", cell.."a", c1.."LRb"}
elseif last(cell) == "L" then
local cell2, recurse = StripChar(cell, "L")
local opp = OppLeft(cell2, recurse+1)
if last2(cell) == "R" then
return {cell.."La", cell.."Ma", cell.."Ra", cell.."Rb", c1.."Ra", cell.."a", opp.."b"}
else
return {cell.."La", cell.."Ma", cell.."Ra", cell.."Rb", c1.."Ma", cell.."a", opp.."b"}
end
elseif isdigit(last(cell)) then
local d = digit(cell)
return {cell.."La", cell.."Ma", cell.."Ra", cell.."Rb", "X"..((d+1)%7).."a", cell.."a", "X"..((d-1)%7).."Rb"}
elseif cell == "X" then
return {"X0a", "X1a", "X2a", "X3a", "X4a", "X5a", "X6a"}
end
return {}
end
--------------------------------------------------------------------------------
function Neighbors(cell)
-- return a list of the cells adjacent to the given cell
-- (callers must not modify the list)
local nbrs = nbrcache[cell]
if not nbrs then
nbrs = FindNeighbors(cell)
nbrcache[cell] = nbrs
end
return nbrs
end
--------------------------------------------------------------------------------
function Formation(cell, live)
-- return the name of the formation of live cells around the given cell
-- (or nil if it isn't found)
local nbrs = Neighbors(cell)
local n = #nbrs
if n ~= 6 and n ~= 7 then return nil end
local f2 = {} -- the neighbor states repeated twice
for k = 1, n do
local state = live[nbrs[k]] and "1" or "0"
f2[k] = state
f2[k+n] = state
end
local f3 = {} -- f2 (minus its last entry) in reverse order
for k = 1, 2*n-1 do f3[k] = f2[2*n-k] end
if n == 7 then
for offset = 0, 6 do
local name = hept_map[table.concat(f2, "", offset+1, offset+7)]
if name then return name end
name = hept_map[table.concat(f3, "", offset+1, offset+7)]
if name then return name end
end
else
for offset = 0, 4, 2 do
local name = hex_map[table.concat(f2, "", offset+1, offset+6)]
if name then return name end
name = hex_map[table.concat(f3, "", offset+1, offset+6)]
if name then return name end
end
end
return nil
end
--------------------------------------------------------------------------------
function Advance(live)
-- return the set of live cells in the next generation
local candidates = {}
for cell in pairs(live) do
candidates[cell] = true
for _, nb in ipairs(Neighbors(cell)) do candidates[nb] = true end
end
local newlive = {}
for cell in pairs(candidates) do
local f = Formation(cell, live)
if f then
if live[cell] then
if rule["S"..f] then newlive[cell] = true end
else
if rule["B"..f] then newlive[cell] = true end
end
end
end
return newlive
end
--------------------------------------------------------------------------------
-- rule strings like "B2o3/S23" are made up of these groups
local outer_total = {"B1", "B2", "B3", "B4", "B5", "B6", "B7", "S0", "S1", "S2", "S3", "S4", "S5", "S6", "S7"}
local total_formations = {3, 7, 10, 8, 5, 2, 1, 2, 3, 7, 10, 8, 5, 2, 1}
local formation_symbol = {"hox", "chompax", "pogchambdx", "champgox", "phomx", "oa", "o",
"oa", "hox", "chompax", "pogchambdx", "champgox", "phomx", "oa", "o"}
local outer_index = {}
for k, s in ipairs(outer_total) do outer_index[s] = k end
--------------------------------------------------------------------------------
function CompressRule()
local groups = {}
for k = 1, #outer_total do groups[k] = {} end
for t in pairs(rule) do
local k = outer_index[t:sub(1,2)]
if k then table.insert(groups[k], t:sub(3,3)) end
end
local s = "B"
for k, letters in ipairs(groups) do
if k == 8 then s = s.."/S" end
if #letters > 0 then
s = s..outer_total[k]:sub(2,2)
if #letters ~= total_formations[k] then
table.sort(letters)
s = s..table.concat(letters)
end
end
end
return s
end
--------------------------------------------------------------------------------
function DecompressRule(s)
-- return the set of transitions in the given rule string
local newrule = {}
local bs, outer = "", "0"
for c = 1, #s do
local ch = s:sub(c,c)
if ch == "B" then
bs = "B"
elseif ch == "/" or ch == "S" then
bs = "S"
elseif ch:find("^[0-7]$") then
outer = ch
local nextch = s:sub(c+1,c+1)
if c == #s or not ("pogchambdx"):find(nextch, 1, true) then
-- no letters follow so include all formations
local k = outer_index[bs..outer]
if not k then error("bad rule") end
for letter in formation_symbol[k]:gmatch(".") do
newrule[bs..outer..letter] = true
end
end
else
newrule[bs..outer..ch] = true
end
end
return newrule
end
--------------------------------------------------------------------------------
function DrawPolygon(xy, fillrgba)
-- fill the given polygon and draw its outline
local n = #xy
local pts = {}
local cx, cy = 0, 0
for k = 1, n, 2 do
pts[k] = floor(xy[k] + 0.5)
pts[k+1] = floor(xy[k+1] + 0.5)
cx = cx + xy[k]
cy = cy + xy[k+1]
end
cx = floor(cx / (n/2) + 0.5)
cy = floor(cy / (n/2) + 0.5)
-- fill a fan of triangles from the center of the polygon
cv.blend(0)
cv.rgba(fillrgba)
for k = 1, n, 2 do
local k2 = k + 2
if k2 > n then k2 = 1 end
cv.fill(cx, cy, pts[k], pts[k+1], pts[k2], pts[k2+1])
cv.line(cx, cy, pts[k], pts[k+1]) -- hide any gaps between triangles
end
-- draw the antialiased outline
pts[n+1] = pts[1]
pts[n+2] = pts[2]
cv.rgba(bordercolor)
cv.blend(1)
cv.line(table.unpack(pts))
cv.blend(0)
end
--------------------------------------------------------------------------------
function InsidePolygon(x, y, xy)
local inside = false
local n = #xy
local jx, jy = xy[n-1], xy[n]
for k = 1, n, 2 do
local ix, iy = xy[k], xy[k+1]
if ((iy > y) ~= (jy > y)) and (x < (jx-ix) * (y-iy) / (jy-iy) + ix) then
inside = not inside
end
jx, jy = ix, iy
end
return inside
end
--------------------------------------------------------------------------------
function CellUnder(x, y)
-- return the drawn cell containing the given canvas position (or nil)
for _, p in ipairs(polys) do
if InsidePolygon(x, y, p.xy) then return p.cell end
end
return nil
end
--------------------------------------------------------------------------------
function CellAt(x, y)
-- return the cell containing the given canvas position
local cell = CellUnder(x, y)
if cell then return cell end
-- not inside any polygon so return the cell with the closest center
local mincell, mind = nil, math.huge
for _, p in ipairs(polys) do
local n = #p.xy
local cx, cy = 0, 0
for k = 1, n, 2 do
cx = cx + p.xy[k]
cy = cy + p.xy[k+1]
end
cx = cx / (n/2)
cy = cy / (n/2)
local d = (cx-x)^2 + (cy-y)^2
if d < mind then
mincell, mind = p.cell, d
end
end
return mincell
end
--------------------------------------------------------------------------------
function Translate(ax, ay, zx, zy)
-- return (z + a) / (1 + conj(a) z), the hyperbolic translation that moves 0 to a
local nx, ny = zx + ax, zy + ay
local dx, dy = 1 + ax*zx + ay*zy, ax*zy - ay*zx
local d = dx*dx + dy*dy
return (nx*dx + ny*dy) / d, (ny*dx - nx*dy) / d
end
--------------------------------------------------------------------------------
function Untranslate(ax, ay, zx, zy)
-- the inverse of Translate (it moves a to 0)
return Translate(-ax, -ay, zx, zy)
end
--------------------------------------------------------------------------------
function HeptInfo(h)
-- return the 7 hexagons around heptagon h (in cyclic order) and the 7 heptagons
-- adjacent to h, where hepts[j] lies between hexes[j] and hexes[j+1]
local info = heptcache[h]
if info then return info end
local hexes = Neighbors(h)
local hepts, index = {}, {}
for j = 1, 7 do
-- a hexagon's neighbors alternate between hexagons and heptagons
local a = Neighbors(hexes[j])
local b = Neighbors(hexes[j%7 + 1])
for _, x in ipairs({a[2], a[4], a[6]}) do
if x ~= h and (x == b[2] or x == b[4] or x == b[6]) then
hepts[j] = x
index[x] = j
end
end
end
info = {hexes = hexes, hepts = hepts, index = index}
heptcache[h] = info
return info
end
--------------------------------------------------------------------------------
function AddAdjacent(h, frames)
-- add frames for any heptagons adjacent to h (which must have a frame)
-- and return a list of the heptagons that were added
local f = frames[h]
local info = HeptInfo(h)
local newhepts = {}
for j = 1, 7 do
local q = info.hepts[j]
if not frames[q] then
local angle = f.a + f.s*(j-1)*hepttheta
local qx, qy = Translate(f.x, f.y, nbrdist*cos(angle), nbrdist*sin(angle))
frames[q] = {x = qx, y = qy}
newhepts[#newhepts+1] = q
end
end
-- find the orientation of each new heptagon from the positions of h
-- and the heptagon after h in its list of adjacent heptagons
for _, q in ipairs(newhepts) do
local qf = frames[q]
local qinfo = HeptInfo(q)
local k = qinfo.index[h]
local nf = frames[qinfo.hepts[k%7 + 1]]
local hx, hy = Untranslate(qf.x, qf.y, f.x, f.y)
local nx, ny = Untranslate(qf.x, qf.y, nf.x, nf.y)
local hangle = math.atan(hy, hx)
if (math.atan(ny, nx) - hangle) % (2*pi) < pi then qf.s = 1 else qf.s = -1 end
qf.a = hangle - qf.s*(k-1)*hepttheta
end
return newhepts
end
--------------------------------------------------------------------------------
function CenterAnchor()
-- if the anchor isn't the heptagon closest to the disk's center (eg. after a fast drag)
-- then keep moving it to whichever adjacent heptagon is closer to the center
local f = {x = anchorx, y = anchory, a = anchora, s = anchors}
for _ = 1, 1000 do
local temp = {[anchor] = f}
AddAdjacent(anchor, temp)
local best, bestr = anchor, f.x*f.x + f.y*f.y
for q, qf in pairs(temp) do
local r = qf.x*qf.x + qf.y*qf.y
if r < bestr then best, bestr = q, r end
end
if best == anchor then break end
anchor, f = best, temp[best]
end
anchorx, anchory, anchora, anchors = f.x, f.y, f.a, f.s
end
--------------------------------------------------------------------------------
function BuildTiling()
-- find the position and orientation of every heptagon within maxdist of the
-- disk's center (and the heptagons adjacent to them)
CenterAnchor()
frames = {[anchor] = {x = anchorx, y = anchory, a = anchora, s = anchors}}
shown = {}
local maxr = tanh(maxdist/2)
local queue = {anchor}
local n = 1
while n <= #queue do
local h = queue[n]
n = n + 1
local f = frames[h]
if f.x*f.x + f.y*f.y <= maxr*maxr then
shown[#shown+1] = h
for _, q in ipairs(AddAdjacent(h, frames)) do
queue[#queue+1] = q
end
end
end
-- make the heptagon closest to the disk's center the new anchor
-- (this doesn't change the view but it avoids losing precision)
local best, bestr = anchor, math.huge
for _, h in ipairs(shown) do
local f = frames[h]
local r = f.x*f.x + f.y*f.y
if r < bestr then best, bestr = h, r end
end
local f = frames[best]
anchor, anchorx, anchory, anchora, anchors = best, f.x, f.y, f.a, f.s
tilingchanged = false
end
--------------------------------------------------------------------------------
function HeptVertices(f)
-- return the 7 vertices of the heptagon with the given frame
-- (vertex k is on the line to adjacent heptagon k)
if not f.verts then
local verts = {}
for k = 1, 7 do
local angle = f.a + f.s*(k-1)*hepttheta
verts[2*k-1], verts[2*k] = Translate(f.x, f.y, vertdist*cos(angle), vertdist*sin(angle))
end
f.verts = verts
end
return f.verts
end
--------------------------------------------------------------------------------
function TransformView(transform)
-- apply the given isometry to the view
-- (we track where the anchor's center and a point in the direction of anchora move to)
local bx, by = Translate(anchorx, anchory, 0.5*cos(anchora), 0.5*sin(anchora))
anchorx, anchory = transform(anchorx, anchory)
bx, by = transform(bx, by)
local lx, ly = Untranslate(anchorx, anchory, bx, by)
anchora = math.atan(ly, lx)
tilingchanged = true
end
--------------------------------------------------------------------------------
function RotateView(cx, cy, angle)
-- rotate the view by the given angle about the point cx,cy
local c, s = cos(angle), sin(angle)
TransformView(function (zx, zy)
local wx, wy = Untranslate(cx, cy, zx, zy)
return Translate(cx, cy, wx*c - wy*s, wx*s + wy*c)
end)
end
--------------------------------------------------------------------------------
function ToScreen(zx, zy)
-- convert a point in the disk to canvas coordinates
local half = scale/2
local mid = scale*0.55
return mid + zy*half, mid - zx*half
end
--------------------------------------------------------------------------------
function FromScreen(x, y)
-- convert canvas coordinates to a point in the disk
local half = scale/2
local mid = scale*0.55
local zx, zy = (mid - y)/half, (x - mid)/half
local r = math.sqrt(zx*zx + zy*zy)
if r > 0.995 then
-- keep the point inside the disk
zx, zy = zx*0.995/r, zy*0.995/r
end
return zx, zy
end
--------------------------------------------------------------------------------
function DrawCell(cell, pts, live, histset)
-- draw the cell with the given vertices (points in the disk)
local xy = {}
local minx, miny, maxx, maxy = math.huge, math.huge, -math.huge, -math.huge
for k = 1, #pts, 2 do
local x, y = ToScreen(pts[k], pts[k+1])
xy[k], xy[k+1] = x, y
minx = math.min(minx, x)
maxx = math.max(maxx, x)
miny = math.min(miny, y)
maxy = math.max(maxy, y)
end
-- don't bother drawing tiny cells near the edge of the disk
if maxx - minx < 2 and maxy - miny < 2 then return end
local hex = Shape(cell) == 6
local color
if live[cell] then
color = hex and oncolor or onheptcolor
elseif histset and histset[cell] then
color = hex and historycolor or historyheptcolor
else
color = hex and offcolor or offheptcolor
end
DrawPolygon(xy, color)
polys[#polys+1] = {cell = cell, xy = xy}
end
--------------------------------------------------------------------------------
function DrawUniverse()
local live = universe[gen]
local histset = nil
if history then
-- find all cells that were alive in earlier generations
histset = {}
for g = 0, gen-1 do
for cell in pairs(universe[g]) do histset[cell] = true end
end
end
if tilingchanged then BuildTiling() end
polys = {}
local drawn = {}
for _, h in ipairs(shown) do
local f = frames[h]
DrawCell(h, HeptVertices(f), live, histset)
-- draw the hexagons around h
local info = HeptInfo(h)
for j = 1, 7 do
local hex = info.hexes[j]
if not drawn[hex] then
drawn[hex] = true
-- hex lies between h and its adjacent heptagons q1 and q2
local j1 = (j-2)%7 + 1
local q1, q2 = info.hepts[j1], info.hepts[j]
local v = HeptVertices(f)
local v1 = HeptVertices(frames[q1])
local v2 = HeptVertices(frames[q2])
local i1 = HeptInfo(q1).index
local i2 = HeptInfo(q2).index
local pts = {}
local function add(verts, k)
pts[#pts+1] = verts[2*k-1]
pts[#pts+1] = verts[2*k]
end
add(v, j1)
add(v, j)
add(v2, i2[h])
add(v2, i2[q1])
add(v1, i1[q2])
add(v1, i1[h])
DrawCell(hex, pts, live, histset)
end
end
end
end
--------------------------------------------------------------------------------
function DrawFormation(pattern, x, y)
-- draw a neighborhood diagram in the rules panel
local cx = 1.15*scale + (scale/5)*x - panelx
local cy = 0.1*scale + (scale/10)*y
local size = scale/10
local xy
local function addpt(p, angle)
xy[#xy+1] = p[1] * sin(angle) * size + cx
xy[#xy+1] = p[1] * -cos(angle) * size + cy
end
local a0, a1, b1 = pt.a0, pt.a1, pt.b1
local a8, a9, b9, c9, d9 = pt.a8, pt.a9, pt.b9, pt.c9, pt.d9
if #pattern == 7 then
local u = pi/7
xy = {}
for k = 0, 6 do addpt(a0, (a0[2]+2*k) * u) end
DrawPolygon(xy, highlightheptcolor)
for gon = 0, 6 do
local g2 = gon*2
xy = {}
addpt(a1, (a1[2]+g2) * u)
addpt(a0, (a0[2]+g2) * u)
addpt(a0, (a0[2]-2+g2) * u)
addpt(a1, (a1[2]-2+g2) * u)
addpt(b1, (b1[2]-2+g2) * u)
addpt(b1, (-b1[2]+g2) * u)
DrawPolygon(xy, pattern:sub(gon+1,gon+1) == "1" and oncolor or offcolor)
end
else
local u = pi/3
xy = {}
for k = 0, 5 do addpt(a8, (a8[2]+k) * u) end
DrawPolygon(xy, highlightcolor)
for gon = 0, 5 do
local on = pattern:sub(gon+1,gon+1) == "1"
xy = {}
if gon % 2 == 0 then
-- hexagon
addpt(a8, (a8[2]-2+gon) * u)
addpt(c9, (c9[2]-2+gon) * u)
addpt(d9, (d9[2]-2+gon) * u)
addpt(d9, (-d9[2]+gon) * u)
addpt(c9, (-c9[2]+gon) * u)
addpt(a8, (-a8[2]+gon) * u)
DrawPolygon(xy, on and oncolor or offcolor)
else
-- heptagon
addpt(a9, (a9[2]+gon-1) * u)
addpt(b9, (b9[2]+gon-1) * u)
addpt(c9, (c9[2]+gon-1) * u)
addpt(a8, (a8[2]+gon-1) * u)
addpt(a8, (-a8[2]+gon-1) * u)
addpt(c9, (-c9[2]+gon-1) * u)
addpt(b9, (-b9[2]+gon-1) * u)
DrawPolygon(xy, on and onheptcolor or offheptcolor)
end
end
end
end
--------------------------------------------------------------------------------
function CreateRulesPanel()
-- draw the neighborhood diagrams into a clip (they never change)
panelx = floor(1.09*scale)
cv.create(math.max(1, viewwd-panelx), viewht, "rulespanel")
local oldtarget = cv.target("rulespanel")
cv.rgba(bgcolor)
cv.blend(0)
cv.fill()
for num, f in ipairs(hept_formations) do
DrawFormation(f[1], (num-1)//9, (num-1)%9)
end
for num, f in ipairs(hex_formations) do
DrawFormation(f[1], (num-1)//10 + 2, (num-1)%10)
end
cv.target(oldtarget)
end
--------------------------------------------------------------------------------
function TruncateHistory()
-- remove all generations after the current one
for g = gen+1, lastgen do universe[g] = nil end
lastgen = gen
end
--------------------------------------------------------------------------------
function SetRule(checkbox)
local t = checkbox.transition
if rule[t] then rule[t] = nil else rule[t] = true end
TruncateHistory()
Refresh()
end
--------------------------------------------------------------------------------
function ResetGeneration()
playing = false
gen = 0
Refresh()
end
--------------------------------------------------------------------------------
function PrevGeneration()
playing = false
if gen > 0 then
gen = gen - 1
Refresh()
end
end
--------------------------------------------------------------------------------
function NextGeneration()
universe[gen+1] = Advance(universe[gen])
gen = gen + 1
if gen > lastgen then lastgen = gen end
end
--------------------------------------------------------------------------------
function StepGeneration()
playing = false
NextGeneration()
Refresh()
end
--------------------------------------------------------------------------------
function TogglePlay()
playing = not playing
if playing then
nextstep = glu.millisecs() + 1000/speed
NextGeneration()
Refresh()
end
end
--------------------------------------------------------------------------------
function ChangeSpeed(delta)
for k, s in ipairs(speedlist) do
if s == speed then
if k+delta >= 1 and k+delta <= #speedlist then
speed = speedlist[k+delta]
end
break
end
end
Refresh()
end
--------------------------------------------------------------------------------
function RandomFillOptions(option)
if option == "big" and radius < 7 then
radius = radius + 1
elseif option == "small" and radius > 0 then
radius = radius - 1
elseif option == "down" and density > 0 then
density = floor((density - 0.05)*20 + 0.5) / 20
elseif option == "up" and density < 1 then
density = floor((density + 0.05)*20 + 0.5) / 20
end
Refresh()
end
--------------------------------------------------------------------------------
function RandomFill(rad, dens)
local live = {}
if math.random() < dens then live["X"] = true end
for offset = 0, 6 do
for num = 1, numcells[rad+1] do
if math.random() < dens then
live["X"..offset..cellnames[num]] = true
end
end
end
universe = {[0] = live}
gen = 0
lastgen = 0
Refresh()
end
--------------------------------------------------------------------------------
function ShowHistory()
history = not history
Refresh()
end
--------------------------------------------------------------------------------
function Origin()
anchor = "X"
anchorx, anchory = 0, 0
anchora = 0
anchors = 1
tilingchanged = true
Refresh()
end
--------------------------------------------------------------------------------
function ExportCode()
local s = CompressRule()
local cells = SortedCells(universe[gen])
if #cells > 0 then s = s.." "..table.concat(cells, " ") end
glu.setclipboard(s)
end
--------------------------------------------------------------------------------
function ImportCode()
playing = false
local code = glu.getclipboard() or ""
local items = {}
for item in code:gmatch("%S+") do items[#items+1] = item end
if #items == 0 or items[1]:sub(1,1) ~= "B" then
glu.warn("Clipboard doesn't contain a HyperConway code.")
return
end
local ok, newrule = pcall(DecompressRule, items[1])
if not ok then
glu.warn("Clipboard contains an invalid rule.")
return
end
local live = {}
for k = 2, #items do
-- ignore any invalid cell names
local cell = items[k]
if cell == "X" or cell:find("^X[0-6][LMR]*[ab]?$") then live[cell] = true end
end
rule = newrule
universe = {[0] = live}
gen = 0
lastgen = 0
Origin() -- calls Refresh()
end
--------------------------------------------------------------------------------
function CanonicalKey(set)
return table.concat(SortedCells(set), " ")
end
--------------------------------------------------------------------------------
function DetectPeriod()
playing = false
local gen_threshold = 512
local pop_threshold = 1024
cv.cursor("wait")
local current = universe[gen]
local index = {[CanonicalKey(current)] = 0}
local pops = {Count(current)}
local title, info
for g = 0, gen_threshold-1 do
current = Advance(current)
local pop = Count(current)
if pop >= pop_threshold then
title = "Potential explosion detected"
info = "The pattern reached a population of "..pop.." in generation "..(gen+g+1)
break
end
local key = CanonicalKey(current)
local k = index[key]
if k then
local period = g - k + 1
local minpop = math.huge
for p = k+1, #pops do minpop = math.min(minpop, pops[p]) end
if pop == 0 then
title = "Spark/diehard detected"
elseif period == 1 then
title = minpop.."-cell still life detected"
else
title = minpop.."P"..period.." detected"
end
info = "The pattern stabilizes on generation "..(gen+k)
break
end
index[key] = g+1
pops[#pops+1] = pop
end
if not title then
local lo, hi = math.huge, 0
for _, p in ipairs(pops) do
lo = math.min(lo, p)
hi = math.max(hi, p)
end
title = "No periodicity detected within "..gen_threshold.." generations"
info = "Lowest population reached: "..lo..", highest population reached: "..hi
end
cv.cursor("arrow")
glu.note(title.."\n\n"..info)
end
--------------------------------------------------------------------------------
function ToggleCell(x, y)
local cell = CellAt(x, y)
if not cell then return end
local live = universe[gen]
if live[cell] then live[cell] = nil else live[cell] = true end
TruncateHistory()
Refresh()
end
--------------------------------------------------------------------------------
function ChangeCenter(x, y, dist)
-- dist is the squared distance from the disk's center (1 is the boundary)
local cell = CellAt(x, y)
if not cell or Shape(cell) ~= 7 or not frames[cell] then return end
local f = frames[cell]
if dist <= 0.05 then
-- clicked the center heptagon so rotate the tiling by pi/7 about its center
RotateView(f.x, f.y, pi/7)
else
-- move the clicked heptagon to the disk's center
local cx, cy = f.x, f.y
local bx, by = Translate(cx, cy, 0.5*cos(f.a), 0.5*sin(f.a))
TransformView(function (zx, zy) return Untranslate(cx, cy, zx, zy) end)
-- then rotate it so its vertices are at multiples of pi/7 (like the origin heptagon)
local lx, ly = Untranslate(cx, cy, bx, by)
local angle = math.atan(ly, lx)
RotateView(0, 0, floor(angle/(pi/7) + 0.5)*(pi/7) - angle)
end
Refresh()
end
--------------------------------------------------------------------------------
function CreateControls()
-- create buttons and check boxes with sizes based on the current scale
cp.buttonht = math.max(14, floor(scale/30 + 0.5))
cp.radius = cp.buttonht // 2
cp.textfontsize = math.max(7, floor(scale/66 + 0.5))
cp.textgap = math.max(6, floor(scale/66 + 0.5))
cp.labelrgba = cp.black
exitbutton = cp.button("Exit", glu.exit)
resetbutton = cp.button("\u{25C4}\u{25C4} ", ResetGeneration)
prevbutton = cp.button("\u{25C4} ", PrevGeneration)
nextbutton = cp.button(" \u{25BA}", StepGeneration)
playbutton = cp.button("Play/Pause", TogglePlay)
speeddown = cp.button("<", ChangeSpeed, {-1})
speedup = cp.button(">", ChangeSpeed, {1})
historybutton = cp.button("History", ShowHistory)
periodbutton = cp.button("Detect period", DetectPeriod)
exportbutton = cp.button("Export to clipboard", ExportCode)
importbutton = cp.button("Import from clipboard", ImportCode)
originbutton = cp.button("Origin", Origin)
randombutton = cp.button("Random", function () RandomFill(radius, density) end)
radiusdown = cp.button("<", RandomFillOptions, {"small"})
radiusup = cp.button(">", RandomFillOptions, {"big"})
densitydown = cp.button("<", RandomFillOptions, {"down"})
densityup = cp.button(">", RandomFillOptions, {"up"})
clearbutton = cp.button("Clear", function () RandomFill(0, 0) end)
-- create a check box for every birth and survival transition
cp.buttonht = math.max(13, floor(scale/35 + 0.5))
cp.radius = cp.buttonht // 4
rulebox = {}
local function addbox(transition, x, y)
local box = cp.checkbox(transition, SetRule)
box.transition = transition
box.px = x
box.py = y
rulebox[#rulebox+1] = box
end
for num, f in ipairs(hept_formations) do
local col, row = (num-1)//9, (num-1)%9
local x = 1.2*scale + col*0.2*scale
if num == 1 then
addbox("S"..f[2], x, 0.085*scale + row*0.1*scale)
else
addbox("B"..f[2], x, 0.070*scale + row*0.1*scale)
addbox("S"..f[2], x, 0.106*scale + row*0.1*scale)
end
end
for num, f in ipairs(hex_formations) do
local col, row = (num-1)//10 + 2, (num-1)%10
local x = 1.2*scale + col*0.2*scale
if num == 1 then
addbox("S"..f[2], x, 0.085*scale + row*0.1*scale)
else
addbox("B"..f[2], x, 0.070*scale + row*0.1*scale)
addbox("S"..f[2], x, 0.106*scale + row*0.1*scale)
end
end
end
--------------------------------------------------------------------------------
function DrawText(s, x, y)
-- draw the given text at x,y
local w, h = cv.text("temptext", s)
cv.paste("temptext", x, y)
end
--------------------------------------------------------------------------------
function CenterText(s, x, y)
-- draw the given text centered at x,y
local w, h = cv.text("temptext", s)
cv.paste("temptext", floor(x - w/2 + 0.5), floor(y - h/2 + 0.5))
end
--------------------------------------------------------------------------------
function DrawControls()
local function px(f) return floor(f*scale + 0.5) end
local gap = math.max(2, scale//200)
local bigfont = math.max(8, floor(scale/50))
cv.font("default-bold", bigfont)
cv.rgba(textcolor)
cv.blend(1)
DrawText("Generation = "..gen, 20, 0.02*scale)
DrawText("Population = "..Count(universe[gen]), 20, 0.05*scale)
local smallfont = math.max(7, floor(scale/70))
cv.font("default-bold", smallfont)
CenterText("HyperConway "..version.." by Valky River", viewwd/2, 0.025*scale)
exitbutton.show((viewwd - exitbutton.wd)/2, 0.045*scale)
if iOS then
originbutton.show(20, px(0.83))
else
CenterText("Right-click center", 1.003*scale, 0.99*scale)
CenterText("heptagon to rotate", 1.001*scale, 1.01*scale)
CenterText("Right-click a near-center", 0.972*scale, 1.04*scale)
CenterText("heptagon to change center", 0.963*scale, 1.06*scale)
originbutton.show(0.963*scale, px(0.93))
end
-- generation buttons
local y = px(0.09)
resetbutton.show(20, y)
prevbutton.show(resetbutton.x + resetbutton.wd + gap, y)
nextbutton.show(prevbutton.x + prevbutton.wd + gap, y)
playbutton.show(20, px(0.14))
speeddown.show(20, px(0.185))
speedup.show(speeddown.x + speeddown.wd + gap, px(0.185))
CenterText(string.format("%g / s", speed), (speeddown.x + speedup.x + speedup.wd)/2,
px(0.195) + cp.buttonht + smallfont)
-- buttons at bottom left
historybutton.show(20, px(0.88))
periodbutton.show(20, px(0.93))
exportbutton.show(20, px(0.98))
importbutton.show(20, px(1.03))
-- random fill buttons at bottom right
randombutton.show(px(1.12), px(0.97))
clearbutton.show(px(1.12) + (randombutton.wd-clearbutton.wd)/2, px(1.02))
local x = math.max(px(1.23), randombutton.x + randombutton.wd + 3*gap)
radiusdown.show(x, px(0.97))
radiusup.show(radiusdown.x + radiusdown.wd + gap, px(0.97))
x = math.max(px(1.31), radiusup.x + radiusup.wd + 3*gap)
densitydown.show(x, px(0.97))
densityup.show(densitydown.x + densitydown.wd + gap, px(0.97))
local rx = (radiusdown.x + radiusup.x + radiusup.wd)/2
local dx = (densitydown.x + densityup.x + densityup.wd)/2
CenterText("Radius", rx, px(1.025))
CenterText(tostring(radius), rx, px(1.045))
CenterText("Density", dx, px(1.025))
CenterText(string.format("%g", density), dx, px(1.045))
-- transition check boxes
cv.rgba(cp.white)
cv.blend(0)
for _, box in ipairs(rulebox) do
local x, y = floor(box.px), floor(box.py)
box.show(x, y, rule[box.transition] == true)
if not rule[box.transition] then
-- make button white so ticked transitions stand out more
cv.flood(x+5, y+5)
end
end
end
--------------------------------------------------------------------------------
function Refresh()
cv.rgba(bgcolor)
cv.blend(0)
cv.fill()
cv.paste("rulespanel", panelx, 0)
DrawUniverse()
DrawControls()
-- the canvas is fully opaque so we can use cv.update()
cv.update()
end
--------------------------------------------------------------------------------
function SetScale()
scale = math.min(viewwd/1.94, viewht/1.1)
end
--------------------------------------------------------------------------------
function CheckViewSize()
-- if the viewport size has changed then resize the canvas and everything in it
local newwd, newht = glu.getview()
if newwd ~= viewwd or newht ~= viewht then
if not iOS and (newwd < minwd or newht < minht) then
-- don't let the viewport get too small
newwd = math.max(newwd, minwd)
newht = math.max(newht, minht)
glu.setview(newwd, newht)
end
viewwd, viewht = newwd, newht
cv.resize(viewwd, viewht)
SetScale()
cp.disable_all() -- prevent old button locations being pressed
CreateControls()
CreateRulesPanel()
Refresh()
end
end
--------------------------------------------------------------------------------
function HandleKey(event)
if event == "key return none" then TogglePlay()
elseif event == "key right none" then StepGeneration()
elseif event == "key left none" then PrevGeneration()
elseif event == "key space none" then StepGeneration()
elseif event == "key z none" then PrevGeneration()
elseif event == "key 0 none" then ResetGeneration()
elseif event == "key escape none" then glu.exit()
end
end
--------------------------------------------------------------------------------
function DiskRadius(x, y)
-- return the distance from the disk's center to x,y (1 is the boundary)
local half = scale/2
local mid = scale*0.55
local dx, dy = (x - mid)/half, (y - mid)/half
return math.sqrt(dx*dx + dy*dy)
end
--------------------------------------------------------------------------------
function DragTiling(x, y, newx, newy)
-- move the tiling so the clicked point x,y stays under the mouse until the button
-- is released (newx,newy is the mouse position that started the drag)
local zx, zy = FromScreen(x, y)
local prevx, prevy = x, y
-- ignore mouse positions too close to the disk's edge (where a tiny mouse movement
-- can move the tiling a huge distance) unless the drag started at least that close
local maxradius = math.min(0.99, math.max(0.95, DiskRadius(x, y)))
while true do
if not newx then
local event = glu.getevent()
if event:find("^mup") then break end
newx, newy = cv.getxy()
end
if newx >= 0 and (newx ~= prevx or newy ~= prevy) and DiskRadius(newx, newy) <= maxradius then
local wx, wy = FromScreen(newx, newy)
-- translate along the line through z and w so that z moves to w
local ux, uy = Untranslate(zx, zy, wx, wy)
local ax, ay = zx, zy
TransformView(function (px, py)
px, py = Untranslate(ax, ay, px, py)
px, py = Translate(ux, uy, px, py)
return Translate(ax, ay, px, py)
end)
zx, zy = wx, wy
prevx, prevy = newx, newy
Refresh()
end
newx = nil
end
end
--------------------------------------------------------------------------------
function ClickOrDrag(x, y)
-- if the mouse moves more than dragmin pixels before the button is released then
-- drag the tiling, otherwise toggle the state of the clicked cell
-- (this also lets Glui users drag the tiling with a finger)
local dragmin = 2
while true do
local event = glu.getevent(0)
if event:find("^mup") then
ToggleCell(x, y)
return
end
local newx, newy = cv.getxy()
if newx >= 0 and (math.abs(newx - x) > dragmin or math.abs(newy - y) > dragmin) then
-- start dragging from the original click so the clicked tile stays under the mouse
DragTiling(x, y, newx, newy)
return
end
end
end
--------------------------------------------------------------------------------
function HandleClick(event)
local _, x, y, button, mods = gp.split(event)
x = tonumber(x)
y = tonumber(y)
local dx = (x - scale*0.55) / (scale/2)
local dy = (y - scale*0.55) / (scale/2)
local dist = dx*dx + dy*dy
if button == "right" or mods == "ctrl" then
if dist <= 0.4 then ChangeCenter(x, y, dist) end
elseif button == "left" then
-- any drawn tile can be clicked or dragged, even a tiny one at the edge of the disk
if CellUnder(x, y) and DiskRadius(x, y) < 0.99 then ClickOrDrag(x, y) end
end
end
--------------------------------------------------------------------------------
function EventLoop()
while true do
local event = cp.process( glu.getevent() )
if #event == 0 then
CheckViewSize()
if playing and glu.millisecs() >= nextstep then
nextstep = glu.millisecs() + 1000/speed
NextGeneration()
Refresh()
end
elseif event:find("^key") then
HandleKey(event)
elseif event:find("^cclick") then
HandleClick(event)
end
end
end
--------------------------------------------------------------------------------
function Main()
glu.settitle("HyperConway "..version)
glu.setoption("getescape", 1)
glu.setcolor("viewport", bgcolor[1], bgcolor[2], bgcolor[3])
math.randomseed(os.time())
if not iOS then glu.setview(viewwd, viewht) end
viewwd, viewht = glu.getview()
cv.create(viewwd, viewht)
SetScale()
CreateControls()
CreateRulesPanel()
Refresh()
EventLoop()
end
--------------------------------------------------------------------------------
function Finish()
cv.delete()
end
--------------------------------------------------------------------------------
gp.call(Main, Finish)