Ahoy navigator! Here be scores of maps revealing the pirates' buried dubloons. Race the gold home afore they return! Use <🔄> to change map/reset. Can you beat 14s in maps 1-3?
for(_="if(Ji.Wr=VJWV='U.fillH🏡'Gh.F,FyE;g=SY=R,yQ)*P-1O(aN[Y++]='🏃'💰')+=)}d.data[MatF600&&=0=a.clientb[fN,r)]s(1,'for(;i=>M.,t(gb[i]cHText(tImageData((m=95*m%k) /k1e331);hypot(/r*(A?1:.3)+(A?.6:.97P;e(=N,r,g,s)=>b=Array(6e5d=c.ge0,0,,c.font='px a';e{g;g<r;)a(g++;f*(r|0)+a|0;j+;k=2**O;l{m=n=a||1;bH(1sa>a+<r>r+<(=2){a=;o=y=*;p=x=*{a++O.5;V(P*4*+3;xsinNPrQcosNPrSRp-x,t=o-yp=x;o=ys(x+Y/g*iQ+t/g*i),g,95,s=[R1];t===1(b[si]YSs[--Y]+1)O)+)-))-2?V:=r,7e5)i]='05786738'[b[i>>2]*4+i%4]*,24e5u={j(x= %Q= %)-i;};v=[];Y;s(uN),v{xQ,r}),9Gh=s(A=1,M='T=D=X=Rt};setInterval({c.pud,0,0JT){X;Vx=x-FxQ=y-FyRxY;t=ytSj(FxYEtJg^A)Rt;FV(A=g)?:'⛵';3+f(FxEP4]}v.map({JFx-WxE-Wy)<&A==1){U++D,WV'';UG&D>8)T,VFV''}Wr,WxO7,Wy`<🔄> #${n} ⌛${X/}`,6,,onclick{T=T|!X;xX;yY;V'❌';y<x<95l(nO+x/|0;l()";G=/[-N-SE-HU-WJ]/.exec(_);)with(_.split(G))_=join(shift());eval(_)
Zm9yKF89ImlmKEppLldyPVZKV1Y9J1UuZmlsbEjwn4+hJ0doLkYsRnlFO2c9U1k9Uix5USkqUC0xTyhhTltZKytdPR8n8J+Pgyce8J+SsCcpHSs9HCl9G2QuZGF0YVsaTWF0Rhk2MDAYJiYXPTAWPWEuY2xpZW50FWJbZk4scildFHMoMSwnE2Zvcig7Emk9PhFNLhAsdChnD2JbaV0OY0hUZXh0KAx0SW1hZ2VEYXRhKAsobT05NSptJWspCQkvawgxZTMHMzEGKTsFGWh5cG90KAQvciooQT8xOi4zKSsoQT8uNjouOTdQAztlKBECPU4scixnLHMpPT4BYj1BcnJheSg2ZTUFZD1jLmdlCzAsMCwHLBgFYy5mb250PScGcHggYSc7ZQF7ZxY7Emc8cjspYShnKysbO2YBByoocnwwKSthfDA7agErFDtrPTIqKgZPO2wBe209bj1hfHwxO2JIKDEFcwFhPgYXYSsGPAcXcj4GF3IrBjwYFygUPTIpAnthPQg7bz15PQgqGDtwPXg9CCoHAnthHAgrCCsITy41O1YoCFAqNCoGKzM7eBwZc2luTlByURwZY29zTlByUwRScC14LHQ9by15BXA9eDtvPXkCcyh4K1kvZyppUSt0L2cqaSksZxssOTUbLAYFcz1bUjFdO3Q9EQ49PTEXKGJbcx9pXRYFEllTc1stLVldDysxKQ9PKQ8rBykPLQcpKQIOLTI/Vg46Dj1yLDdlNSkCGmldPScwNTc4NjczOCdbYltpPj4yXSo0K2klNF0qBiwyNGU1BXU9EXsSaih4PQklB1E9CSUYKS1pOwV9O3Y9W107WRY7cwEodU4pLHYfe3hRLHJ9KQITHSw5BRNHBWg9cyhBPTEsHgVNPRMnBVQ9RD1YPVJ0Fn07c2V0SW50ZXJ2YWwoEXtjLnB1C2QsMCwwBUpUKXtYHAY7VgR4PRB4LUZ4UT0QeS1GeQVSeANZO3Q9eQN0U2ooRngcWUUcdAVKZ15BKVJ0FjtGVihBPWcpPx46J+KbtSc7GjMrZihGeEVQNF0WfXYubWFwKBF7SgRGeC1XeEUtV3kpPAYmQT09MSl7VR0rK0QsV1YnJztVRyZEPjgpVBYsEFZGVicnfQxXcixXeE83LFd5GwUMYDzwn5SEPiAjJHtufSDijJske1gvB31gLDYsBhssBgVvbmNsaWNrAXtUPVR8IVg7EHgVWDsQeRVZOxBWJ+KdjCc7EHk8BhcQeDw5NRdsKG5PKxB4LwZ8MBs7bCgpIjtHPS9bAS0fTi1TRS1IVS1XSl0vLmV4ZWMoXyk7KXdpdGgoXy5zcGxpdChHKSlfPWpvaW4oc2hpZnQoKSk7ZXZhbChfKQ==
/* The Plundering Sailsman
*
* 1024 byte game featuring timed navigation challenges in a procedurally
* generated archipelago.
*
* Notes:
*
* It had to fit in 1024 bytes after packing, so expect tortured syntax and
* repetition for compression.
*
* Unreserved globals with a static literal value are removed and their
* values put in-place by the build script to save variables. The rest are
* assigned single character names.
*
* A C preprocessor is used to inline the random number generator.
*
*/
SEA = 0
LAND = 1
CONTOUR = 2
COIN = "💰"
RUN = "🏃"
SAIL = "⛵"
HOME = "🏡"
/* 31 is a recurring constant.
*/
MARGIN = 31
FRAME_INTERVAL = 31
/* Dimensions are chosen for their compact literal representations.
*/
W = 1e3
H = 6e2
AREA = 6e5
AREA_MUL_4 = 24e5
// The working terrain which serves as the terrain map and an ImageData.
terrain = Array(AREA);
image = c.getImageData(0,0,W,H);
// The "a" is there to make the CSS parse
c.font = "31px a"
// g initialised outside the loop helps compression.
repeat=(a,r,g,s)=> { g=0; for(;g<r;) a(g++); }
// Returns the grid index of some possibly non-integer coordinates (a,r)=(x,y)
index=(a,r,g,s)=>W*(r|0)+a|0
// Gets grid value of some coordinates or 0 (SEA) if out-of-bounds.
get=(a,r,g,s)=>+terrain[index(a,r)]
/* The maps need a seedable random number generator. This is a Lehmer LCG with
* prime modulus 2^31-1 and a small multiplier to save space. In JS you could
* write the modulus shorter as ~1e25 but it doesn't help in this case because
* 31 and -1 get compressed.
*/
RNG_M = 2**31-1
RNG_A = 95
#define urand() ((rng_state=RNG_A*rng_state%RNG_M)/RNG_M)
#define irand(x) ((rng_state=RNG_A*rng_state%RNG_M)%x)
// map generation
generate_map=(a,r,g,s)=>{
rng_state = map_id = a||1;
terrain.fill(LAND)
// s plots a pixel if it's within the margin
s=(a,r,g,s)=>
a>MARGIN
&&a+MARGIN<W
&&r>MARGIN
&&r+MARGIN<H
&&(terrain[index(a,r)]=CONTOUR);
// generate pseudorandom coastline contours
repeat(i=>{
a = urand()
Y=y=urand()*H
X=x=urand()*W
repeat(i=>{
a += urand()+urand()+urand()-1.5 // approx normal angle peturbation
r = urand()**4 * 31 + 3 // exponential random step
x += Math.sin(a) * r,
y += Math.cos(a) * r
// stroke the line from X,Y to x,y
g = Math.hypot(u=X-x,v=Y-y)
X = x
Y = y
repeat(i=>s(x+u/g*i,y+v/g*i),g);
},95)
},31);
// flood the perimeter with sea
s = [u=1]
v=i=>terrain[i]==1&&(terrain[s[u++]=i]=0);
for(;u;
g = s[--u],
v(g+1),
v(g-1),
v(g+W),
v(g-W));
// filter out the contours, leaving just sea and land
repeat(i=>terrain[i]-CONTOUR ? r=terrain[i] : terrain[i]=r,7e5);
// map the terrain through a palette into the ImageData
repeat(i=>image.data[i]="05786738"[terrain[i>>2]*4+i%4]*31,AREA_MUL_4);
// scatter the gameplay objects on appropriate terrain
random_point=i=>{for(;get(x=irand(W),y=irand(H))-i;);}
objects = [];
u=0;
s=(a,r,g,s)=>(random_point(a),objects[u++]={x,y,r});
repeat(i=>s(LAND, COIN),9);
s(LAND, HOME);
player=s(t=LAND, RUN);
goal=s(LAND,'');
// initialise game state
running = score = time = u = v = 0
}
setInterval(i=>{
// draw the map background
c.putImageData(image, 0, 0);
// game step
if(running) {
time += 31;
// apply terrain-specific friction and impulse towards the goal
r = Math.hypot(x=goal.x-player.x,y=goal.y-player.y)
u = x / r * (t?1:.3) + (t?.6:.97) * u;
v = y / r * (t?1:.3) + (t?.6:.97) * v;
// update player x/y and terrain
g = get(player.x+=u, player.y+=v);
// kill velocity if transitioning from sea <-> land
if(g^t)u=v=0;
// set the player image based on its terrain
player.r = (t=g) ? RUN : SAIL;
// trace the route (knocks out the alpha component to show the white bg)
image.data[3+index(player.x, player.y)*4]=0;
}
objects.map(i=>{
// check for proximity to the player (coins and home)
if(Math.hypot(player.x-i.x, player.y-i.y)<31 & t==LAND) {
if(i.r == COIN)
++score,
i.r='';
if(i.r == HOME & score>8)
running=0,
goal.r=player.r='';
}
// draw object
c.fillText(i.r, i.x - 17, i.y)
})
// draw the map status
c.fillText(`<🔄> #${map_id} ⌛${time/1e3}`,6,MARGIN)
}, FRAME_INTERVAL)
// start level on click if paused, and operate a minimal UI
onclick=(a,r,g,s)=>{
// start game loop on click if it's not already running
running=running|!time
// move the goal the the mouse coords
goal.x = a.clientX
goal.y = a.clientY
goal.r = "❌"
// treat the upper corner as 3 buttons which adjust the map seed by -1 / 0 / +1
goal.y<MARGIN && goal.x<95 && generate_map(map_id-1+goal.x/31|0);
}
generate_map()