nx_econsim_app.nx source
↩ module page · 117 lines · 17697 B
1// nx_econsim_app.nx -- PLAYABLE in-browser Capitalism Lab client for /econsim.
2// Organ-emitted (nx_vroom_html seam, no hand-written client); JS mirrors the
3// gated nx_caplab_* organs. R5 multi-product portfolio + R6 cities/map (Expand)
4// + R7 FINANCE (take loans up to a credit line, pay interest monthly, repay, go
5// BANKRUPT if net worth sinks below your credit). license_tier: ORIGINAL
6
7import "nx_syscalls.nx"
8
9const EA_OUT: *u8 = "web_assets/econsim.html"
10const EA_CAP: i64 = 262144
11
12func ea(dst: *u8, off: i64, s: *u8) -> i64 {
13 var o: i64 = off
14 var i: i64 = 0
15 while s[i] != 0 as u8 { dst[o] = s[i]; o = o + 1; i = i + 1 }
16 return o
17}
18
19func main() -> i64 {
20 let h: *u8 = sys_mmap(EA_CAP)
21 var w: i64 = 0
22
23 w = ea(h, w, "<!doctype html><html lang='en'><head><meta charset='utf-8'>\n" as *u8)
24 w = ea(h, w, "<meta name='viewport' content='width=device-width,initial-scale=1'>\n" as *u8)
25 w = ea(h, w, "<title>EconSim - Capitalism Lab</title>\n" as *u8)
26 w = ea(h, w, "<style>\n" as *u8)
27 w = ea(h, w, ":root{--bg:#0a0e16;--pan:#121a27;--pan2:#16202f;--ink:#e8eef6;--mut:#8fa6bd;--acc:#ffd24c;--blu:#4cc2ff;--good:#3fb950;--bad:#f85149;--line:#26344a}\n" as *u8)
28 w = ea(h, w, "*{box-sizing:border-box}body{margin:0;background:radial-gradient(1200px 600px at 70% -10%,#16243a,var(--bg));color:var(--ink);font:15px/1.5 system-ui,Segoe UI,Roboto,sans-serif}\n" as *u8)
29 w = ea(h, w, "header{display:flex;align-items:center;gap:18px;flex-wrap:wrap;padding:16px 22px;border-bottom:1px solid var(--line);background:#0c1320}\n" as *u8)
30 w = ea(h, w, "h1{margin:0;font-size:21px}h1 span{color:var(--acc);font-weight:700}\n" as *u8)
31 w = ea(h, w, ".hud{margin-left:auto;display:flex;gap:20px;align-items:center;font-size:14px;color:var(--mut)}.hud b{color:var(--ink);font-size:18px}\n" as *u8)
32 w = ea(h, w, ".money{color:var(--good)}.neg{color:var(--bad)}\n" as *u8)
33 w = ea(h, w, "main{display:grid;grid-template-columns:340px 1fr 1fr;gap:16px;padding:16px 22px;max-width:1200px;margin:0 auto}\n" as *u8)
34 w = ea(h, w, ".panel{background:linear-gradient(180deg,var(--pan),var(--pan2));border:1px solid var(--line);border-radius:14px;padding:16px 18px}\n" as *u8)
35 w = ea(h, w, ".panel h2{margin:0 0 12px;font-size:14px;text-transform:uppercase;letter-spacing:.6px;color:var(--mut)}\n" as *u8)
36 w = ea(h, w, ".prodcard{background:#0e1726;border:1px solid var(--line);border-radius:11px;padding:10px 13px;margin-bottom:11px}\n" as *u8)
37 w = ea(h, w, ".prodcard h3{margin:0 0 4px;font-size:14px;color:var(--acc)}\n" as *u8)
38 w = ea(h, w, "label{display:block;margin:9px 0;font-size:12.5px;color:var(--mut)}label output{float:right;color:var(--blu);font-weight:700;font-size:14px}\n" as *u8)
39 w = ea(h, w, "input[type=range]{width:100%;margin-top:6px;accent-color:var(--blu)}\n" as *u8)
40 w = ea(h, w, ".stat{margin:8px 0 2px;font-size:12px;color:var(--mut)}.stat b{color:var(--ink)}\n" as *u8)
41 w = ea(h, w, ".finbox{margin:4px 0 0;padding:10px 12px;background:#0e1726;border:1px solid var(--line);border-radius:10px}.finrow{font-size:12.5px;color:var(--mut);margin-bottom:4px}.finrow b{color:var(--ink)}\n" as *u8)
42 w = ea(h, w, "button{cursor:pointer;border:0;border-radius:10px;padding:10px 13px;font-weight:700;font-size:13.5px;margin:8px 6px 0 0}button:disabled{opacity:.45;cursor:default}\n" as *u8)
43 w = ea(h, w, "#advance{background:var(--acc);color:#1a1500;width:100%}#advance:hover{filter:brightness(1.08)}#advance:disabled{background:#3a3f49;color:#8a8f99}#reset{background:#26344a;color:var(--ink)}\n" as *u8)
44 w = ea(h, w, "#loan{background:var(--blu);color:#04223a;font-size:12px;padding:7px 11px}#repay{background:#26344a;color:var(--ink);font-size:12px;padding:7px 11px}.exp{background:var(--blu);color:#04223a;padding:5px 10px;font-size:12px;margin:6px 0 0}\n" as *u8)
45 w = ea(h, w, ".msg{min-height:20px;margin:12px 0 0;color:var(--acc);font-weight:600}\n" as *u8)
46 w = ea(h, w, ".catblock{margin-bottom:14px}.cathead{font-size:13px;font-weight:700;margin-bottom:6px;color:var(--ink)}\n" as *u8)
47 w = ea(h, w, ".bar{margin:7px 0}.bar .row{display:flex;justify-content:space-between;font-size:12.5px;margin-bottom:3px}.bar .t{height:13px;border-radius:7px;background:#0c1320;overflow:hidden}.bar .f{height:100%;border-radius:7px;transition:width .5s ease}\n" as *u8)
48 w = ea(h, w, "ol#board{list-style:none;margin:0;padding:0;counter-reset:r}ol#board li{counter-increment:r;display:flex;justify-content:space-between;padding:9px 10px;border-bottom:1px solid var(--line);border-radius:8px}ol#board li::before{content:counter(r);color:var(--acc);font-weight:700;margin-right:10px}ol#board li.you{background:#13233a}.bk{color:var(--bad);font-weight:700;font-size:11px}\n" as *u8)
49 w = ea(h, w, ".chartwrap{grid-column:1 / -1}canvas{width:100%;height:220px;display:block}\n" as *u8)
50 w = ea(h, w, "footer{max-width:1200px;margin:0 auto;padding:6px 22px 36px;color:var(--mut);font-size:12px}\n" as *u8)
51 w = ea(h, w, "@media(max-width:900px){main{grid-template-columns:1fr}}\n" as *u8)
52 w = ea(h, w, "</style></head><body>\n" as *u8)
53
54 w = ea(h, w, "<header><h1>EconSim <span>Capitalism Lab</span></h1>\n" as *u8)
55 w = ea(h, w, "<div class='hud'><div>Month <b id='month'>1</b>/12</div><div>Cash <b id='cash' class='money'>$1000</b></div><div>Debt <b id='debt' class='neg'>$0</b></div><div>Rank <b id='rank'>-</b></div></div></header>\n" as *u8)
56
57 w = ea(h, w, "<main>\n" as *u8)
58 w = ea(h, w, "<section class='panel'><h2>Your portfolio</h2><div id='ctrls'></div><div id='fin' class='finbox'></div>\n" as *u8)
59 w = ea(h, w, "<button id='advance'>Advance Month ▶</button><button id='reset'>New Game</button>\n" as *u8)
60 w = ea(h, w, "<p id='msg' class='msg'></p></section>\n" as *u8)
61 w = ea(h, w, "<section class='panel'><h2>Your map (cities)</h2><div id='cities'></div></section>\n" as *u8)
62 w = ea(h, w, "<section class='panel'><h2>Leaderboard (total cash)</h2><ol id='board'></ol></section>\n" as *u8)
63 w = ea(h, w, "<section class='panel chartwrap'><h2>Cash over the year</h2><canvas id='chart'></canvas></section>\n" as *u8)
64 w = ea(h, w, "</main>\n" as *u8)
65 w = ea(h, w, "<footer>Sovereign Nishi stack · mirrors the gated <code>nx_caplab_*</code> organs (market, competition, supply, tick-loop, quality+brand, products, cities, finance) · nishifamily.com/econsim</footer>\n" as *u8)
66
67 w = ea(h, w, "<script>\n" as *u8)
68 w = ea(h, w, "var CATS=[{n:'Soda',choke:100,baseM:120},{n:'Shoes',choke:80,baseM:60}];\n" as *u8)
69 w = ea(h, w, "var CITIES=[{n:'Metro',pop:200},{n:'Town',pop:50}];\n" as *u8)
70 w = ea(h, w, "var WP=1,WQ=2,WB=1,BASE=15,MONTHS=12,START=1000,CAP=100,EXPAND=250,RATE=8,CREDIT=2000,LOAN=500;\n" as *u8)
71 w = ea(h, w, "var COL=['#4cc2ff','#f85149','#3fb950'];\n" as *u8)
72 w = ea(h, w, "function uc(q){return BASE+Math.floor(q/5);}\n" as *u8)
73 w = ea(h, w, "function adc(b){return b*2;}\n" as *u8)
74 w = ea(h, w, "function cityM(ci,cj){return Math.floor(CATS[ci].baseM*CITIES[cj].pop/100);}\n" as *u8)
75 w = ea(h, w, "function appeal(p,ck){var pv=ck-p.price;if(pv<0)pv=0;var a=WP*pv+WQ*p.quality+WB*p.brand;return a<0?0:a;}\n" as *u8)
76 w = ea(h, w, "function splitCap(ps,ck,M,caps){var wv=ps.map(function(p,i){return caps[i]>0?appeal(p,ck):0;});var sum=wv.reduce(function(a,b){return a+b;},0);if(sum<=0)return ps.map(function(){return 0;});var base=wv.map(function(x){return Math.floor(M*x/sum);});var rem=wv.map(function(x,i){return M*x-base[i]*sum;});var used=base.reduce(function(a,b){return a+b;},0);var left=M-used;var got=ps.map(function(){return false;});while(left>0){var bi=-1,br=-1;for(var i=0;i<ps.length;i++){if(!got[i]&&rem[i]>br){br=rem[i];bi=i;}}got[bi]=true;base[bi]++;left--;}for(var j=0;j<ps.length;j++){if(base[j]>caps[j])base[j]=caps[j];}return base;}\n" as *u8)
77 w = ea(h, w, "function prod(p,q,b){return {price:p,quality:q,brand:b,sold:0};}\n" as *u8)
78 w = ea(h, w, "var firms,month,hist;\n" as *u8)
79 w = ea(h, w, "function newGame(){firms=[{name:'You',you:true,cash:START,debt:0,bankrupt:false,cities:[1,0],prods:[prod(50,0,0),prod(40,0,0)]},{name:'BudgetCo',strat:'d',cash:START,debt:0,bankrupt:false,cities:[1,1],prods:[prod(40,0,0),prod(32,0,0)]},{name:'PrestigeInc',strat:'p',cash:START,debt:0,bankrupt:false,cities:[1,1],prods:[prod(66,45,35),prod(55,30,20)]}];month=1;hist=firms.map(function(f){return [f.cash];});buildControls();render();}\n" as *u8)
80 w = ea(h, w, "function ai(f){f.prods.forEach(function(p,ci){if(f.strat==='d'){p.price=Math.max(uc(p.quality)+1,(ci===0?42:34)-(month%4));}if(f.strat==='p'){p.quality=Math.min(100,(ci===0?45:30)+month);p.brand=Math.min(100,(ci===0?35:20)+month);}});}\n" as *u8)
81 w = ea(h, w, "function readControls(){var me=firms[0];CATS.forEach(function(c,ci){me.prods[ci].price=+document.getElementById('p'+ci).value;me.prods[ci].quality=+document.getElementById('q'+ci).value;me.prods[ci].brand=+document.getElementById('b'+ci).value;});}\n" as *u8)
82 w = ea(h, w, "function advance(){if(month>MONTHS||firms[0].bankrupt)return;readControls();firms.forEach(function(f){if(!f.you)ai(f);});CATS.forEach(function(c,ci){firms.forEach(function(f){f.prods[ci].sold=0;});CITIES.forEach(function(city,cj){var ps=firms.map(function(f){return f.prods[ci];});var caps=firms.map(function(f){return (f.cities[cj]&&!f.bankrupt)?CAP:0;});var dem=splitCap(ps,c.choke,cityM(ci,cj),caps);firms.forEach(function(f,fi){f.prods[ci].sold+=dem[fi];f.cash+=(f.prods[ci].price-uc(f.prods[ci].quality))*dem[fi];});});firms.forEach(function(f){f.cash-=adc(f.prods[ci].brand);});});firms.forEach(function(f){f.cash-=Math.floor(f.debt*RATE/100);if(f.cash-f.debt<=0-CREDIT){f.bankrupt=true;}});hist.forEach(function(hh,i){hh.push(firms[i].cash);});month++;render();if(month>MONTHS)done();}\n" as *u8)
83 w = ea(h, w, "function takeLoan(){var me=firms[0];if(me.bankrupt)return;if(me.debt+LOAN>CREDIT){msg.textContent='Credit limit ($'+CREDIT+') reached.';return;}me.cash+=LOAN;me.debt+=LOAN;render();}\n" as *u8)
84 w = ea(h, w, "function repay(){var me=firms[0];var pay=LOAN;if(pay>me.debt)pay=me.debt;if(pay>me.cash)pay=me.cash;if(pay<0)pay=0;me.cash-=pay;me.debt-=pay;render();}\n" as *u8)
85 w = ea(h, w, "function expand(cj){if(firms[0].cities[cj]||firms[0].bankrupt)return;if(firms[0].cash<EXPAND){msg.textContent='Not enough cash to expand ($'+EXPAND+') - try a loan.';return;}firms[0].cash-=EXPAND;firms[0].cities[cj]=1;render();}\n" as *u8)
86 w = ea(h, w, "function rankOf(){var s=firms.slice().sort(function(a,b){return (b.cash-b.debt)-(a.cash-a.debt);});return s.indexOf(firms[0])+1;}\n" as *u8)
87 w = ea(h, w, "function done(){advBtn.disabled=true;var r=rankOf();msg.textContent=(r===1?'You WON the year! #1 by net worth. ':'Year over - you finished #'+r+'. ')+'New Game to try a new strategy.';}\n" as *u8)
88 w = ea(h, w, "function buildControls(){var me=firms[0];var html='';CATS.forEach(function(c,ci){var p=me.prods[ci];html+=\"<div class='prodcard'><h3>\"+c.n+\"</h3>\";html+=\"<label>Price <output id='po\"+ci+\"'>\"+p.price+\"</output><input id='p\"+ci+\"' type='range' min='16' max='100' value='\"+p.price+\"'></label>\";html+=\"<label>Quality <output id='qo\"+ci+\"'>\"+p.quality+\"</output><input id='q\"+ci+\"' type='range' min='0' max='100' value='\"+p.quality+\"'></label>\";html+=\"<label>Brand <output id='bo\"+ci+\"'>\"+p.brand+\"</output><input id='b\"+ci+\"' type='range' min='0' max='100' value='\"+p.brand+\"'></label>\";html+=\"<div class='stat'>unit cost <b id='uc\"+ci+\"'>\"+uc(p.quality)+\"</b> · ad/mo <b id='ad\"+ci+\"'>\"+adc(p.brand)+\"</b> · margin <b id='mg\"+ci+\"'>\"+(p.price-uc(p.quality))+\"</b></div></div>\";});ctrls.innerHTML=html;CATS.forEach(function(c,ci){['p','q','b'].forEach(function(k){document.getElementById(k+ci).addEventListener('input',function(){labels();citiesPanel();});});});}\n" as *u8)
89 w = ea(h, w, "function labels(){CATS.forEach(function(c,ci){var pr=+document.getElementById('p'+ci).value,q=+document.getElementById('q'+ci).value,b=+document.getElementById('b'+ci).value;document.getElementById('po'+ci).value=pr;document.getElementById('qo'+ci).value=q;document.getElementById('bo'+ci).value=b;document.getElementById('uc'+ci).textContent=uc(q);document.getElementById('ad'+ci).textContent=adc(b);document.getElementById('mg'+ci).textContent=(pr-uc(q));});}\n" as *u8)
90 w = ea(h, w, "function finPanel(){var me=firms[0];var nw=me.cash-me.debt;var it=Math.floor(me.debt*RATE/100);var canLoan=(me.debt+LOAN<=CREDIT)&&!me.bankrupt;var html=\"<div class='finrow'>Debt <b class='\"+(me.debt>0?'neg':'')+\"'>$\"+me.debt+\"</b> · Net worth <b class='\"+(nw<0?'neg':'money')+\"'>$\"+nw+\"</b> · Interest/mo <b>$\"+it+\"</b> (credit $\"+CREDIT+\" @ \"+RATE+\"%)</div>\";html+=\"<button id='loan'\"+(canLoan?'':' disabled')+\">Take Loan ($\"+LOAN+\")</button><button id='repay'>Repay ($\"+LOAN+\")</button>\";fin_.innerHTML=html;document.getElementById('loan').addEventListener('click',takeLoan);document.getElementById('repay').addEventListener('click',repay);}\n" as *u8)
91 w = ea(h, w, "function previewProds(){return CATS.map(function(c,ci){return prod(+document.getElementById('p'+ci).value,+document.getElementById('q'+ci).value,+document.getElementById('b'+ci).value);});}\n" as *u8)
92 w = ea(h, w, "function citiesPanel(){var me=previewProds();var html='';CITIES.forEach(function(city,cj){var mine=0,tot=0;CATS.forEach(function(c,ci){var ps=firms.map(function(f,fi){return fi===0?me[ci]:f.prods[ci];});var caps=firms.map(function(f){return (f.cities[cj]&&!f.bankrupt)?CAP:0;});var dem=splitCap(ps,c.choke,cityM(ci,cj),caps);mine+=dem[0];tot+=dem.reduce(function(a,b){return a+b;},0);});var here=firms[0].cities[cj];var pct=tot>0?Math.round(100*mine/tot):0;html+=\"<div class='catblock'><div class='cathead'>\"+city.n+\" (pop \"+city.pop+\")</div>\";if(here){html+=\"<div class='bar'><div class='row'><span>your share</span><span>\"+pct+\"% · \"+mine+\" sold</span></div><div class='t'><div class='f' style='width:\"+pct+\"%;background:#3fb950'></div></div></div>\";}else{html+=\"<div class='stat'>Not operating here. <button class='exp' data-c='\"+cj+\"'>Expand ($\"+EXPAND+\")</button></div>\";}html+=\"</div>\";});cities_.innerHTML=html;var bs=cities_.getElementsByClassName('exp');for(var i=0;i<bs.length;i++){bs[i].addEventListener('click',function(){expand(+this.getAttribute('data-c'));});}}\n" as *u8)
93 w = ea(h, w, "function board(){var s=firms.slice().sort(function(a,b){return (b.cash-b.debt)-(a.cash-a.debt);});var html='';s.forEach(function(f){var cls=f.you?\" class='you'\":'';var tag=f.bankrupt?\" <span class='bk'>BANKRUPT</span>\":'';html+='<li'+cls+\"><span>\"+f.name+tag+\"</span><span class='\"+((f.cash-f.debt)<0?'neg':'money')+\"'>$\"+(f.cash-f.debt)+\"</span></li>\";});board_.innerHTML=html;}\n" as *u8)
94 w = ea(h, w, "function chart(){var c=document.getElementById('chart');var dpr=window.devicePixelRatio||1;var W=c.clientWidth,H=220;c.width=W*dpr;c.height=H*dpr;var x=c.getContext('2d');x.scale(dpr,dpr);x.clearRect(0,0,W,H);var all=[].concat.apply([],hist);var mx=Math.max.apply(null,all),mn=Math.min.apply(null,all);if(mn>0)mn=0;var pad=40;var span=(mx-mn)||1;var n=hist[0].length;function px(i){return pad+(W-pad-10)*(n<2?0:i/(n-1));}function py(v){return H-22-(H-44)*(v-mn)/span;}x.strokeStyle='#26344a';x.beginPath();x.moveTo(pad,py(0));x.lineTo(W-10,py(0));x.stroke();x.fillStyle='#8fa6bd';x.font='11px system-ui';x.fillText('$'+mx,2,py(mx)+4);x.fillText('$'+mn,2,py(mn)+4);hist.forEach(function(hh,fi){x.strokeStyle=COL[fi];x.lineWidth=2.4;x.beginPath();hh.forEach(function(v,i){if(i===0)x.moveTo(px(i),py(v));else x.lineTo(px(i),py(v));});x.stroke();});}\n" as *u8)
95 w = ea(h, w, "function render(){labels();finPanel();month_.textContent=Math.min(month,MONTHS);cash.textContent='$'+firms[0].cash;cash.className=firms[0].cash<0?'neg':'money';debtEl.textContent='$'+firms[0].debt;rank.textContent='#'+rankOf();var over=(month>MONTHS)||firms[0].bankrupt;advBtn.disabled=over;if(firms[0].bankrupt){msg.textContent='BANKRUPT - you are out of the game. Hit New Game.';}else if(month<=MONTHS){msg.textContent='';}citiesPanel();board();chart();}\n" as *u8)
96 w = ea(h, w, "var ctrls=document.getElementById('ctrls'),fin_=document.getElementById('fin'),cities_=document.getElementById('cities'),board_=document.getElementById('board'),advBtn=document.getElementById('advance'),msg=document.getElementById('msg'),month_=document.getElementById('month'),cash=document.getElementById('cash'),debtEl=document.getElementById('debt'),rank=document.getElementById('rank');\n" as *u8)
97 w = ea(h, w, "advBtn.addEventListener('click',advance);document.getElementById('reset').addEventListener('click',function(){advBtn.disabled=false;newGame();});window.addEventListener('resize',function(){chart();});\n" as *u8)
98 w = ea(h, w, "newGame();\n" as *u8)
99 w = ea(h, w, "</script>\n" as *u8)
100 w = ea(h, w, "</body></html>\n" as *u8)
101
102 let fd: i64 = sys_openat_wr(EA_OUT, 0x1a4)
103 if fd < 0 { sys_write(2, "FATAL: cannot open econsim.html\n" as *u8, 32); sys_exit(2); return 2 }
104 sys_write(fd, h, w)
105 sys_close(fd)
106 sys_write(1, "[nx_econsim_app] wrote econsim.html bytes=" as *u8, 42)
107 let nb: *u8 = sys_mmap(28)
108 var k: i64 = 0
109 var m: i64 = w
110 if m == 0 { nb[0] = 48 as u8; k = 1 }
111 while m > 0 { nb[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
112 var q: i64 = k - 1
113 while q >= 0 { let o1: *u8 = sys_mmap(1); o1[0] = nb[q]; sys_write(1, o1, 1); q = q - 1 }
114 sys_write(1, "\n" as *u8, 1)
115 sys_exit(0)
116 return 0
117}