| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="utf-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| <title>SSOA — Agent‑Based Simulation (HTML+JS)</title> |
| <style> |
| :root{--bg:#0b0e16;--fg:#e6edf3;--muted:#8892a6;--accent:#7aa2f7;--good:#3fb950;--warn:#f2cc60;--bad:#f85149;} |
| html,body{height:100%;margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;background:var(--bg);color:var(--fg);} |
| .row{display:flex;gap:16px;align-items:stretch} |
| .col{display:flex;flex-direction:column;gap:12px} |
| .card{background:#11162a;border:1px solid #1e263d;border-radius:14px;padding:12px} |
| h1{font-size:20px;margin:8px 0 4px} |
| h2{font-size:16px;margin:8px 0} |
| label{font-size:12px;color:var(--muted)} |
| input[type="range"]{width:100%} |
| input[type="number"],select{background:#0f1425;color:var(--fg);border:1px solid #1e263d;border-radius:8px;padding:6px} |
| button{background:#182040;color:var(--fg);border:1px solid #243056;border-radius:10px;padding:8px 10px;cursor:pointer} |
| button:hover{background:#1a274d} |
| .metrics{display:flex;gap:10px;flex-wrap:wrap;font-size:12px;color:#cdd6e4} |
| .badge{background:#0f1425;border:1px solid #1e263d;border-radius:999px;padding:4px 8px} |
| canvas{background:#0a0f1f;border:1px solid #1e263d;border-radius:14px} |
| .grid{display:grid;grid-template-columns:repeat(2,minmax(180px,1fr));gap:12px} |
| .hint{font-size:12px;color:var(--muted)} |
| .small{font-size:11px;color:var(--muted)} |
| .mono{font-family:ui-monospace,SFMono-Regular,Consolas,Monaco,monospace} |
| </style> |
| </head> |
| <body> |
| <div class="row" style="padding:12px"> |
| <div class="col" style="flex:1 1 340px;max-width:430px"> |
| <div class="card"> |
| <h1>Secret Societies Optimisation Algorithm (SSOA)</h1> |
| <div class="hint">Agent‑based simulation with hierarchical lodges → cells, secrecy‑masked reporting, grand directives, covert exchange, infiltration, purge, and oath.</div> |
| </div> |
| <div class="card"> |
| <h2>Controls</h2> |
| <div class="grid"> |
| <div> |
| <label>Objective</label> |
| <select id="objective"> |
| <option value="sphere">Sphere (2‑D)</option> |
| <option value="rastrigin">Rastrigin (2‑D)</option> |
| <option value="ackley">Ackley (2‑D)</option> |
| </select> |
| </div> |
| <div> |
| <label>Bounds</label> |
| <select id="bounds"> |
| <option value="sphere">[-5, 5]</option> |
| <option value="wide">[-10, 10]</option> |
| <option value="narrow">[-3, 3]</option> |
| </select> |
| </div> |
| <div> |
| <label>Population N</label> |
| <input id="N" type="number" min="12" max="400" step="1" value="90" /> |
| </div> |
| <div> |
| <label>Lodges L</label> |
| <input id="L" type="number" min="1" max="9" step="1" value="3" /> |
| </div> |
| <div> |
| <label>Cells per Lodge C</label> |
| <input id="C" type="number" min="1" max="8" step="1" value="3" /> |
| </div> |
| <div> |
| <label>Agents per Cell m</label> |
| <input id="m" type="number" min="2" max="12" step="1" value="10" /> |
| </div> |
| <div> |
| <label>Secrecy s</label> |
| <input id="s" type="range" min="0" max="1" step="0.05" value="0.35" /> |
| <span class="small mono" id="sVal">0.35</span> |
| </div> |
| <div> |
| <label>Directive α (start→end)</label> |
| <input id="alpha0" type="number" min="0" max="1" step="0.05" value="0.12" /> |
| <input id="alpha1" type="number" min="0" max="1" step="0.05" value="0.6" /> |
| </div> |
| <div> |
| <label>Drift ρ₀</label> |
| <input id="rho0" type="number" min="0.01" max="2" step="0.01" value="0.4" /> |
| </div> |
| <div> |
| <label>Decay γ</label> |
| <input id="gamma" type="number" min="0.90" max="0.999" step="0.001" value="0.985" /> |
| </div> |
| <div> |
| <label>Covert exchange p<sub>cx</sub></label> |
| <input id="pcx" type="number" min="0" max="0.5" step="0.01" value="0.06" /> |
| </div> |
| <div> |
| <label>Infiltration p<sub>inf</sub></label> |
| <input id="pinf" type="number" min="0" max="0.5" step="0.01" value="0.12" /> |
| </div> |
| <div> |
| <label>Diversity threshold</label> |
| <input id="divThresh" type="number" min="0" max="2" step="0.01" value="0.12" /> |
| </div> |
| <div> |
| <label>Purge ratio π</label> |
| <input id="pi" type="number" min="0" max="0.5" step="0.01" value="0.08" /> |
| </div> |
| <div> |
| <label>Epochs per sec</label> |
| <input id="speed" type="number" min="1" max="240" step="1" value="60" /> |
| </div> |
| </div> |
| <div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:8px"> |
| <button id="startBtn">Start</button> |
| <button id="pauseBtn">Pause</button> |
| <button id="stepBtn">Step</button> |
| <button id="resetBtn">Reset</button> |
| <button id="seedBtn">New Seed</button> |
| </div> |
| <div class="metrics" id="metrics" style="margin-top:8px"></div> |
| </div> |
| <div class="card"> |
| <h2>Notes</h2> |
| <div class="hint">Visual pane maps 2‑D decision space. Colour indicates lodge membership; subtle inner ring indicates cell leaders. Chart shows best objective value across epochs. Projection preserves bounds. Diversity metric uses median pairwise distance normalised by domain width.</div> |
| </div> |
| </div> |
| <div class="col" style="flex:2 1 680px"> |
| <div class="card"> |
| <h2>Search Space</h2> |
| <canvas id="field" width="800" height="520"></canvas> |
| </div> |
| <div class="card"> |
| <h2>Best Fitness</h2> |
| <canvas id="chart" width="800" height="220"></canvas> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| (function(){ |
| |
| const RNG = (seed)=>{ |
| let t = seed >>> 0; |
| return ()=>{ |
| t += 0x6D2B79F5; |
| let r = Math.imul(t ^ t >>> 15, 1 | t); |
| r ^= r + Math.imul(r ^ r >>> 7, 61 | r); |
| return ((r ^ r >>> 14) >>> 0) / 4294967296; |
| } |
| }; |
| const choice = (rng, arr)=>arr[(rng()*arr.length)|0]; |
| const clamp = (x,a,b)=>Math.max(a,Math.min(b,x)); |
| const projBox = (x, lo, hi)=>x.map((v,i)=>clamp(v,lo[i],hi[i])); |
| const add = (a,b)=>[a[0]+b[0], a[1]+b[1]]; |
| const sub = (a,b)=>[a[0]-b[0], a[1]-b[1]]; |
| const mul = (a,s)=>[a[0]*s, a[1]*s]; |
| const had = (a,b)=>[a[0]*b[0], a[1]*b[1]]; |
| const norm = (a)=>Math.hypot(a[0],a[1]); |
| const randn = (rng)=>{ |
| let u = 1 - rng(); |
| let v = 1 - rng(); |
| return Math.sqrt(-2*Math.log(u)) * Math.cos(2*Math.PI*v); |
| }; |
| |
| |
| function makeObjective(name){ |
| if(name==='sphere'){ |
| return { |
| f:(x)=>x[0]*x[0]+x[1]*x[1], |
| label:'Sphere' |
| }; |
| } |
| if(name==='rastrigin'){ |
| return { |
| f:(x)=>20 + (x[0]*x[0]-10*Math.cos(2*Math.PI*x[0])) + (x[1]*x[1]-10*Math.cos(2*Math.PI*x[1])), |
| label:'Rastrigin' |
| }; |
| } |
| |
| return { |
| f:(x)=>{ |
| const a=20,b=0.2,c=2*Math.PI; |
| const s = 0.5*(x[0]*x[0]+x[1]*x[1]); |
| const q = 0.5*(Math.cos(c*x[0])+Math.cos(c*x[1])); |
| return -a*Math.exp(-b*Math.sqrt(s)) - Math.exp(q) + a + Math.E; |
| }, |
| label:'Ackley' |
| }; |
| } |
| |
| |
| class SSOA { |
| constructor(cfg){ |
| Object.assign(this, cfg); |
| this.dim = 2; |
| this.rng = RNG(cfg.seed||123456); |
| this.obj = makeObjective(cfg.objective); |
| this.bounds = cfg.bounds; |
| this.epoch = 0; |
| this.makePopulation(); |
| this.best = {x:[...this.pop[0].x], f:Infinity}; |
| this.evaluateAll(); |
| this.initDiversity = this.diversity(); |
| this.history = []; |
| } |
| makePopulation(){ |
| const [lo,hi] = this.bounds; |
| const N = this.L*this.C*this.m; |
| this.pop = Array.from({length:N}, (_,i)=>({ |
| x:[ lo[0] + (hi[0]-lo[0])*this.rng(), lo[1] + (hi[1]-lo[1])*this.rng() ], |
| f:Infinity, |
| lodge: Math.floor(i/(this.C*this.m)), |
| cell: Math.floor((i/this.m)%this.C) |
| })); |
| } |
| evaluateAll(){ |
| for(const a of this.pop){ a.f = this.obj.f(a.x); } |
| this.updateBest(); |
| } |
| updateBest(){ |
| for(const a of this.pop){ if(a.f < this.best.f){ this.best = {x:[...a.x], f:a.f}; } } |
| } |
| lodgeMembers(L){ return this.pop.filter(a=>a.lodge===L); } |
| cellMembers(L,C){ return this.pop.filter(a=>a.lodge===L && a.cell===C); } |
| leaders(){ |
| |
| const cellBest = []; |
| for(let L=0; L<this.L; L++){ |
| for(let C=0; C<this.C; C++){ |
| const mem = this.cellMembers(L,C); |
| let b = mem[0]; |
| for(const a of mem){ if(a.f<b.f) b=a; } |
| cellBest.push(b); |
| } |
| } |
| const lodgeBest = []; |
| for(let L=0; L<this.L; L++){ |
| const mem = this.lodgeMembers(L); |
| let b = mem[0]; |
| for(const a of mem){ if(a.f<b.f) b=a; } |
| lodgeBest.push(b); |
| } |
| return {cellBest, lodgeBest}; |
| } |
| mask(x){ |
| |
| const m = [0,0]; |
| let k = (this.s*this.dim)|0; if(k<1) k=1; |
| const idx = [0,1]; |
| for(let i=0;i<k;i++) m[idx.splice((this.rng()*idx.length)|0,1)[0]] = 1; |
| return [x[0]*m[0], x[1]*m[1]]; |
| } |
| aggregateBeacon(){ |
| const {lodgeBest} = this.leaders(); |
| |
| let acc=[0,0], cnt=[0,0]; |
| for(const a of lodgeBest){ |
| const M = this.mask(a.x); |
| if(M[0]!==0){ acc[0]+=M[0]; cnt[0]++; } |
| if(M[1]!==0){ acc[1]+=M[1]; cnt[1]++; } |
| } |
| const g=[0,0]; |
| g[0]=cnt[0]? acc[0]/cnt[0] : 0; |
| g[1]=cnt[1]? acc[1]/cnt[1] : 0; |
| return g; |
| } |
| recruitment(){ |
| |
| const [lo,hi] = this.bounds; |
| const width=[hi[0]-lo[0], hi[1]-lo[1]]; |
| const rt = this.rho0*Math.pow(this.gamma, this.epoch); |
| const {lodgeBest} = this.leaders(); |
| const recruits=[]; |
| for(const s of lodgeBest){ |
| const jitter=[(this.rng()*2-1)*rt*width[0], (this.rng()*2-1)*rt*width[1]]; |
| const cand = projBox(add(s.x, jitter), lo, hi); |
| recruits.push({x:cand, f: this.obj.f(cand)}); |
| } |
| return recruits; |
| } |
| initiation(recruits){ |
| |
| const pool=[]; |
| for(const r of recruits){ pool.push(r); } |
| |
| for(let L=0; L<this.L; L++){ |
| for(let C=0; C<this.C; C++){ |
| const mem = this.cellMembers(L,C); |
| const challenger = choice(this.rng, pool); |
| |
| const idx = (this.rng()*mem.length)|0; |
| const inc = mem[idx]; |
| const winnerBetter = challenger.f < inc.f; |
| if(winnerBetter){ |
| inc.x = [...challenger.x]; inc.f = challenger.f; |
| } |
| } |
| } |
| } |
| ritualDrift(){ |
| const [lo,hi] = this.bounds; const width=[hi[0]-lo[0], hi[1]-lo[1]]; |
| const rt = this.rho0*Math.pow(this.gamma, this.epoch); |
| const {cellBest} = this.leaders(); |
| for(const a of this.pop){ |
| const leader = cellBest.find(b=>b.lodge===a.lodge && b.cell===a.cell) || a; |
| const noise=[randn(this.rng)*rt*width[0], randn(this.rng)*rt*width[1]]; |
| const pull= mul(sub(leader.x, a.x), this.beta); |
| const x1 = projBox(add(add(a.x, noise), pull), lo, hi); |
| a.x = x1; a.f = this.obj.f(a.x); |
| } |
| } |
| directives(){ |
| const g = this.aggregateBeacon(); |
| const [lo,hi] = this.bounds; |
| const a = this.alphaSchedule(); |
| for(const x of this.pop){ |
| const step = mul(sub(g, x.x), a); |
| const y = projBox(add(x.x, step), lo, hi); |
| x.x = y; x.f = this.obj.f(y); |
| } |
| } |
| covertExchange(){ |
| if(this.rng() > this.pcx) return; |
| if(this.L<2) return; |
| const L1 = (this.rng()*this.L)|0; let L2 = (this.rng()*this.L)|0; if(L2===L1) L2=(L2+1)%this.L; |
| const A = this.lodgeMembers(L1), B = this.lodgeMembers(L2); |
| const a = choice(this.rng,A), b = choice(this.rng,B); |
| |
| const i = (this.rng()*2)|0; |
| const tmp = a.x[i]; a.x[i]=b.x[i]; b.x[i]=tmp; |
| a.f=this.obj.f(a.x); b.f=this.obj.f(b.x); |
| } |
| infiltration(){ |
| |
| const d = this.diversity(); |
| if(d >= this.divThresh) return; |
| if(this.rng() > this.pinf) return; |
| const [lo,hi] = this.bounds; |
| |
| const L = (this.rng()*this.L)|0, C=(this.rng()*this.C)|0; |
| const mem = this.cellMembers(L,C); |
| let worst = mem[0]; |
| for(const a of mem){ if(a.f > worst.f) worst=a; } |
| const {lodgeBest} = this.leaders(); |
| const s = choice(this.rng, lodgeBest); |
| const rt = this.rho0*Math.pow(this.gamma, this.epoch); |
| const jitter=[(this.rng()*2-1)*rt, (this.rng()*2-1)*rt]; |
| const cand = projBox(add(s.x, jitter), lo, hi); |
| worst.x = cand; worst.f = this.obj.f(cand); |
| } |
| purge(){ |
| const M = Math.max(1, Math.floor(this.pi * this.pop.length)); |
| const sorted = [...this.pop].sort((a,b)=>a.f-b.f); |
| const survivors = sorted.slice(0, this.pop.length - M); |
| const purged = sorted.slice(this.pop.length - M); |
| |
| const {lodgeBest} = this.leaders(); |
| const [lo,hi] = this.bounds; const width=[hi[0]-lo[0], hi[1]-lo[1]]; |
| const rt = 0.3*this.rho0*Math.pow(this.gamma, this.epoch); |
| for(const _ of purged){ |
| if(this.rng()<0.5){ |
| const s = choice(this.rng, lodgeBest); |
| const jitter=[randn(this.rng)*rt*width[0], randn(this.rng)*rt*width[1]]; |
| survivors.push({x:projBox(add(s.x,jitter), lo, hi), f:Infinity, lodge:0, cell:0}); |
| } else { |
| |
| survivors.push({x:[ lo[0]+(hi[0]-lo[0])*this.rng(), lo[1]+(hi[1]-lo[1])*this.rng() ], f:Infinity, lodge:0, cell:0}); |
| } |
| } |
| |
| const N = survivors.length; |
| for(let i=0;i<N;i++){ survivors[i].lodge = Math.floor(i/(this.C*this.m)); survivors[i].cell = Math.floor((i/this.m)%this.C); } |
| this.pop = survivors; |
| this.evaluateAll(); |
| } |
| oath(){ this.updateBest(); } |
| alphaSchedule(){ |
| |
| const t = Math.min(1, this.epoch/1000); |
| const w = 0.5 - 0.5*Math.cos(Math.PI*t); |
| return this.alpha0*(1-w) + this.alpha1*w; |
| } |
| diversity(){ |
| |
| const [lo,hi]=this.bounds; const width = Math.hypot(hi[0]-lo[0], hi[1]-lo[1]); |
| const pts = this.pop.map(a=>a.x); |
| const dists=[]; const M=pts.length; |
| for(let i=0;i<M;i++) for(let j=i+1;j<M;j++) dists.push(Math.hypot(pts[i][0]-pts[j][0], pts[i][1]-pts[j][1])); |
| dists.sort((a,b)=>a-b); |
| const med = dists.length? dists[(dists.length/2)|0] : 0; |
| return med/width; |
| } |
| step(){ |
| this.epoch++; |
| const recruits = this.recruitment(); |
| this.initiation(recruits); |
| this.ritualDrift(); |
| this.directives(); |
| this.covertExchange(); |
| this.infiltration(); |
| this.purge(); |
| this.oath(); |
| this.history.push(this.best.f); |
| if(this.history.length>800) this.history.shift(); |
| } |
| } |
| |
| |
| const field = document.getElementById('field'); |
| const chart = document.getElementById('chart'); |
| const ctx = field.getContext('2d'); |
| const gtx = chart.getContext('2d'); |
| |
| function render(sim){ |
| |
| ctx.clearRect(0,0,field.width,field.height); |
| |
| const W=field.width, H=field.height; |
| const cells=40; const cw=W/cells, ch=H/cells; |
| const [lo,hi]=sim.bounds; |
| for(let i=0;i<cells;i++){ |
| for(let j=0;j<cells;j++){ |
| const x=lo[0] + (i+0.5)/cells*(hi[0]-lo[0]); |
| const y=lo[1] + (j+0.5)/cells*(hi[1]-lo[1]); |
| const v = sim.obj.f([x,y]); |
| const shade = Math.max(0, Math.min(1, 1 - 1/(1+v))); |
| ctx.fillStyle = `rgba(122,162,247,${0.06*shade})`; |
| ctx.fillRect(i*cw, H-(j+1)*ch, cw, ch); |
| } |
| } |
| |
| ctx.strokeStyle='#243056'; ctx.lineWidth=1; |
| ctx.strokeRect(0,0,W,H); |
| |
| const colours=['#7aa2f7','#9ece6a','#f7768e','#bb9af7','#e0af68','#2ac3de','#f7768e']; |
| const [loB,hiB]=sim.bounds; const sx=W/(hiB[0]-loB[0]), sy=H/(hiB[1]-loB[1]); |
| const X=(p)=> (p[0]-loB[0])*sx; const Y=(p)=> H - (p[1]-loB[1])*sy; |
| |
| |
| for(let L=0; L<sim.L; L++){ |
| const col = colours[L%colours.length]; |
| ctx.fillStyle=col; ctx.strokeStyle=col; |
| |
| const {cellBest} = sim.leaders(); |
| const leaders = cellBest.filter(a=>a.lodge===L); |
| |
| const mem = sim.lodgeMembers(L); |
| for(const a of mem){ |
| const r=3.2; |
| ctx.beginPath(); ctx.arc(X(a.x), Y(a.x), r, 0, Math.PI*2); ctx.fill(); |
| } |
| |
| ctx.strokeStyle='#e6edf3'; |
| for(const b of leaders){ |
| ctx.beginPath(); ctx.arc(X(b.x), Y(b.x), 6, 0, Math.PI*2); ctx.stroke(); |
| } |
| } |
| |
| ctx.fillStyle= '#3fb950'; |
| star(ctx, X(sim.best.x), Y(sim.best.x), 6, 12, 5); |
| |
| |
| gtx.clearRect(0,0,chart.width,chart.height); |
| gtx.strokeStyle = '#7aa2f7'; gtx.lineWidth=2; |
| const arr = sim.history; if(arr.length>1){ |
| const min = Math.min(...arr), max = Math.max(...arr); |
| const pad = 0.001; |
| gtx.beginPath(); |
| for(let i=0;i<arr.length;i++){ |
| const x = i/(arr.length-1)*chart.width; |
| const y = chart.height - ((arr[i]-min)/Math.max(pad,(max-min))) * chart.height; |
| if(i===0) gtx.moveTo(x,y); else gtx.lineTo(x,y); |
| } |
| gtx.stroke(); |
| } |
| } |
| function star(g,x,y,r1,r2,points){ |
| g.beginPath(); |
| for(let i=0;i<points*2;i++){ |
| const r = i%2? r1: r2; |
| const a = i*Math.PI/points; |
| const px = x + Math.cos(a)*r; |
| const py = y + Math.sin(a)*r; |
| if(i===0) g.moveTo(px,py); else g.lineTo(px,py); |
| } |
| g.closePath(); g.fill(); |
| } |
| |
| |
| const $ = (id)=>document.getElementById(id); |
| const sVal = $('sVal'); |
| $('s').addEventListener('input', e=>{ sVal.textContent=Number(e.target.value).toFixed(2); }); |
| |
| let sim = null; let running=false; let last=0; let acc=0; |
| function cfgFromUI(seed){ |
| const boundsSel = $('bounds').value; |
| let bounds = { |
| 'sphere':[[-5, -5],[5,5]], 'wide':[[-10,-10],[10,10]], 'narrow':[[-3,-3],[3,3]] |
| }[boundsSel]; |
| return { |
| objective: $('objective').value, |
| bounds: bounds, |
| L: Number($('L').value|0), |
| C: Number($('C').value|0), |
| m: Number($('m').value|0), |
| rho0: Number($('rho0').value), |
| gamma: Number($('gamma').value), |
| s: Number($('s').value), |
| alpha0: Number($('alpha0').value), |
| alpha1: Number($('alpha1').value), |
| pcx: Number($('pcx').value), |
| pinf: Number($('pinf').value), |
| divThresh: Number($('divThresh').value), |
| pi: Number($('pi').value), |
| seed: seed ?? Math.floor(Math.random()*1e9) |
| }; |
| } |
| function reset(seed){ |
| |
| const Nreq = Number($('L').value)*Number($('C').value)*Number($('m').value); |
| $('N').value = Nreq; |
| sim = new SSOA(cfgFromUI(seed)); |
| render(sim); |
| updateMetrics(); |
| } |
| function updateMetrics(){ |
| const m = document.getElementById('metrics'); |
| const div = sim.diversity(); |
| m.innerHTML = ` |
| <span class="badge">epoch <span class="mono">${sim.epoch}</span></span> |
| <span class="badge">best f <span class="mono">${sim.best.f.toExponential(3)}</span></span> |
| <span class="badge">diversity <span class="mono">${div.toFixed(3)}</span></span> |
| <span class="badge">α <span class="mono">${sim.alphaSchedule().toFixed(3)}</span></span> |
| <span class="badge">seed <span class="mono">${sim.rng().toString().slice(2)}</span></span> |
| `; |
| } |
| $('startBtn').onclick=()=>{ running=true; }; |
| $('pauseBtn').onclick=()=>{ running=false; }; |
| $('stepBtn').onclick=()=>{ running=false; tick(1/60,true); }; |
| $('resetBtn').onclick=()=>{ running=false; reset(); }; |
| $('seedBtn').onclick=()=>{ reset(Math.floor(Math.random()*1e9)); }; |
| |
| function tick(dt,force){ |
| const targetHz = Number($('speed').value); |
| acc += dt*targetHz; |
| while(acc>=1 || force){ |
| sim.step(); |
| acc-=1; if(force) break; |
| } |
| render(sim); |
| updateMetrics(); |
| } |
| function loop(ts){ |
| const dt = Math.min(0.1, (ts-last)/1000 || 0); last=ts; |
| if(running){ tick(dt,false); } |
| requestAnimationFrame(loop); |
| } |
| reset(123456789); |
| requestAnimationFrame(loop); |
| })(); |
| </script> |
| </body> |
| </html> |
|
|