[verified] Snake 0.8: circular arena and larger 32px tiles
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
const assert=require('node:assert/strict'), fs=require('node:fs'), zlib=require('node:zlib')
|
||||
const a=require('../src/common/snake.js').ARENA
|
||||
for(const name of fs.readdirSync('src/common/tex').filter(n=>n.endsWith('.png'))) {
|
||||
const b=fs.readFileSync('src/common/tex/'+name)
|
||||
const size=name==='board.png'?a.radius*2:a.cell
|
||||
assert.equal(b.readUInt32BE(16),size,name+' width');assert.equal(b.readUInt32BE(20),size,name+' height')
|
||||
}
|
||||
console.log('NATIVE ART DIMENSIONS PASS')
|
||||
+14
-1
@@ -8,7 +8,20 @@ function rule(name) {
|
||||
return Object.fromEntries(m[1].split(';').filter(s=>s.includes(':')).map(s=>s.trim().split(/:\s*/)))
|
||||
}
|
||||
function px(r,k) { assert.match(r[k] || '', /^\d+px$/, k); return Number(r[k].slice(0,-2)) }
|
||||
for (const name of ['board','hud','footer','overlay']) {
|
||||
const arena = require('../src/common/snake.js').ARENA
|
||||
assert.equal(px(rule('seg'),'width'),arena.cell)
|
||||
assert.equal(px(rule('seg'),'height'),arena.cell)
|
||||
assert.equal(px(rule('food'),'width'),arena.cell)
|
||||
const board=rule('board')
|
||||
assert.equal(px(board,'width'),arena.radius*2)
|
||||
assert.equal(px(board,'height'),arena.radius*2)
|
||||
assert.equal(px(board,'left')+arena.radius,233)
|
||||
assert.equal(px(board,'top')+arena.radius,233)
|
||||
for(const c of arena.cells) for(const name of ['hud','footer']) {
|
||||
const r=rule(name), x=arena.offset+c.x*arena.cell,y=arena.offset+c.y*arena.cell
|
||||
assert.ok(x+arena.cell<=px(r,'left') || x>=px(r,'left')+px(r,'width') || y+arena.cell<=px(r,'top') || y>=px(r,'top')+px(r,'height'), 'legal cell never overlaps '+name)
|
||||
}
|
||||
for (const name of ['hud','footer','overlay']) {
|
||||
const r = rule(name), x=px(r,'left'), y=px(r,'top'), w=px(r,'width'), h=px(r,'height')
|
||||
const dy = Math.max(Math.abs(y-233), Math.abs(y+h-233))
|
||||
const chord = 2*Math.sqrt(233*233-dy*dy)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Create circular board and static preview from the real JS geometry and PNG art.
|
||||
Preview is a composited illustration, NOT a Vela runtime screenshot.
|
||||
Existing sprite art is nearest-neighbor enlarged to native 32px, not blurred.
|
||||
"""
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
a = json.loads(subprocess.check_output(['node', '-p', "JSON.stringify(require('./src/common/snake.js').ARENA)"], cwd=ROOT))
|
||||
tex = ROOT / 'src/common/tex'
|
||||
for p in tex.glob('*.png'):
|
||||
if p.name != 'board.png':
|
||||
img = Image.open(p).convert('RGBA')
|
||||
if img.size != (a['cell'], a['cell']):
|
||||
img.resize((a['cell'], a['cell']), Image.Resampling.NEAREST).save(p)
|
||||
S=4
|
||||
size=a['radius']*2
|
||||
board=Image.new('RGBA',(size*S,size*S),(0,0,0,0))
|
||||
d=ImageDraw.Draw(board)
|
||||
d.ellipse((0,0,size*S-1,size*S-1),fill='#14251e',outline='#36513c',width=2*S)
|
||||
local=a['offset']-(233-a['radius'])
|
||||
for c in a['cells']:
|
||||
x=(local+c['x']*a['cell'])*S;y=(local+c['y']*a['cell'])*S
|
||||
d.rectangle((x,y,x+a['cell']*S-1,y+a['cell']*S-1),fill=('#1d3227' if (c['x']+c['y'])%2 else '#20362a'))
|
||||
board=board.resize((size,size),Image.Resampling.LANCZOS)
|
||||
board.save(tex/'board.png')
|
||||
# Read actual CSS text placement/colors instead of inventing a separate layout.
|
||||
import re
|
||||
ux=(ROOT/'src/pages/index/index.ux').read_text()
|
||||
def rule(name):
|
||||
body=re.search(r'\.'+name+r'\s*\{([^}]+)\}',ux)[1]
|
||||
return dict(re.findall(r'([\w-]+):\s*([^;]+)',body))
|
||||
def px(r,key): return int(r[key].removesuffix('px'))
|
||||
font=subprocess.check_output(['fc-match','-f','%{file}','sans'],text=True)
|
||||
canvas=Image.new('RGB',(980,526),'#10131a')
|
||||
for panel,mask in enumerate([False,True]):
|
||||
watch=Image.new('RGBA',(466,466),'black');watch.alpha_composite(board,(233-a['radius'],233-a['radius']))
|
||||
wd=ImageDraw.Draw(watch)
|
||||
if mask:
|
||||
for c in a['cells']:
|
||||
x=a['offset']+c['x']*a['cell'];y=a['offset']+c['y']*a['cell']
|
||||
wd.rectangle((x,y,x+a['cell']-1,y+a['cell']-1),outline='#71967a')
|
||||
cells=[{'x':7,'y':4},{'x':6,'y':4},{'x':5,'y':4},{'x':5,'y':5},{'x':4,'y':5},{'x':3,'y':5}]
|
||||
names=json.loads(subprocess.check_output(['node','-p',f"JSON.stringify(require('./src/common/snake.js').renderPieces({json.dumps(cells)},{{x:1,y:0}}))"],cwd=ROOT))
|
||||
for c,name in zip(cells,names):
|
||||
watch.alpha_composite(Image.open(tex/(name+'.png')),(a['offset']+c['x']*a['cell'],a['offset']+c['y']*a['cell']))
|
||||
watch.alpha_composite(Image.open(tex/'apple.png'),(a['offset']+8*a['cell'],a['offset']+6*a['cell']))
|
||||
for parent,entries in [('hud',[('score','SCORE 3'),('best','BEST 253')]),('footer',[('modeLabel','CLASSIC'),('hint','swipe / tap pause')])]:
|
||||
pr=rule(parent); y=px(pr,'top')
|
||||
for cls,text in entries:
|
||||
r=rule(cls); f=ImageFont.truetype(font,px(r,'font-size'))
|
||||
wd.text((233,y+px(r,'height')/2),text,font=f,fill=r['color'],anchor='mm')
|
||||
y+=px(r,'height')
|
||||
circle=Image.new('L',(466,466));ImageDraw.Draw(circle).ellipse((0,0,465,465),fill=255)
|
||||
canvas.paste(watch.convert('RGB'),(12+panel*490,12),circle)
|
||||
cd=ImageDraw.Draw(canvas)
|
||||
cd.text((20,490),'STATIC ART / GEOMETRY COMPOSITE — NOT WATCH RUNTIME',fill='white',font=ImageFont.truetype(font,16))
|
||||
cd.text((510,490),'Legal-cell overlay: 89 cells, 32px tiles',fill='white',font=ImageFont.truetype(font,16))
|
||||
canvas.save(ROOT/'tests/preview-0.8-static.png')
|
||||
print(ROOT/'tests/preview-0.8-static.png')
|
||||
# Alpha corners prove the replacement board is circular, not a square frame.
|
||||
assert all(board.getpixel(p)[3]==0 for p in [(0,0),(size-1,0),(0,size-1),(size-1,size-1)])
|
||||
print('Circular PNG alpha corners verified; native 32px sprites generated.')
|
||||
+9
-9
@@ -7,13 +7,13 @@ const snake = require('../src/common/snake.js');
|
||||
const ux = fs.readFileSync('src/pages/index/index.ux', 'utf8');
|
||||
const body = ux.match(/<script>([\s\S]*?)<\/script>/)[1]
|
||||
.replace(/import\s+\w+\s+from\s+'[^']*';?/g, '')
|
||||
.replace(/^const\s+\w+\s*=[^\n]*\n/gm, '')
|
||||
.replace('export default', '');
|
||||
.replace('export default', 'globalThis.spec =');
|
||||
|
||||
const sandbox = { best: require('../src/common/best.js'), storage: {get() {}, set() {}}, snake, Date, Math, String, Number, console, setInterval: () => 0, clearInterval: () => {},
|
||||
CELL: 20, GRID: 16, OFFSET: 73 };
|
||||
};
|
||||
vm.createContext(sandbox);
|
||||
const spec = vm.runInContext('(' + body + ')', sandbox);
|
||||
vm.runInContext(body, sandbox);
|
||||
const spec = sandbox.spec;
|
||||
|
||||
function mk() {
|
||||
const o = Object.assign({}, spec, JSON.parse(JSON.stringify(spec.private)));
|
||||
@@ -29,10 +29,10 @@ assert.match(c.snake[0].src, /head_e\.png$/, 'head texture');
|
||||
assert.match(c.snake[1].src, /body_h\.png$/, 'body texture');
|
||||
assert.match(c.snake[2].src, /tail_w\.png$/, 'tail texture');
|
||||
assert.equal(c.scoreText, 'SCORE 0');
|
||||
assert.equal(c.snake[0].left, (73 + 6 * 20) + 'px', 'head x mapped to px');
|
||||
assert.equal(c.snake[0].left, (snake.ARENA.offset + 6 * snake.ARENA.cell) + 'px', 'head x mapped to px');
|
||||
|
||||
for (let i = 0; i < 5; i++) c.tick();
|
||||
assert.equal(c.model[0].x, 11, 'head moved right 5 steps from x=6');
|
||||
for (let i = 0; i < 1; i++) c.tick();
|
||||
assert.equal(c.model[0].x, 7, 'head moved right one step from x=6');
|
||||
assert.equal(c.over, false);
|
||||
|
||||
c.onSwipe({ direction: 'down' });
|
||||
@@ -47,8 +47,8 @@ c.tick();
|
||||
assert.equal(c.dir.x, 0, 'reversal keeps x');
|
||||
assert.equal(c.dir.y, 1, 'reversal blocked');
|
||||
|
||||
// run into the wall to trigger game over (GRID=16, last column x=15)
|
||||
c.model = [{ x: 15, y: 5 }, { x: 14, y: 5 }, { x: 13, y: 5 }];
|
||||
// Hit the actual circular boundary.
|
||||
c.model = [{ x: 10, y: 5 }, { x: 9, y: 5 }, { x: 8, y: 5 }];
|
||||
c.dir = { x: 1, y: 0 };
|
||||
c.pending = null;
|
||||
c.tick();
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
@@ -5,19 +5,19 @@ let seed=83971
|
||||
const rand=()=>{seed=(Math.imul(seed,1664525)+1013904223)>>>0;return seed/4294967296}
|
||||
const dirs=[{x:1,y:0},{x:0,y:1},{x:-1,y:0},{x:0,y:-1}]
|
||||
let moves=0, seams=0
|
||||
for(const mode of ['classic','wrap']) for(let game=0;game<1000;game++) {
|
||||
for(const board of [16,snake.ARENA]) for(const mode of ['classic','wrap']) for(let game=0;game<1000;game++) {
|
||||
let cells=snake.initialSnake(),dir=dirs[0]
|
||||
for(let tick=0;tick<100;tick++) {
|
||||
dir=snake.turn(dir,dirs[Math.floor(rand()*4)])
|
||||
const h=cells[0]
|
||||
const food=rand()<0.3?{x:(h.x+dir.x+16)%16,y:(h.y+dir.y+16)%16}:{x:0,y:0}
|
||||
const r=snake.step(cells,dir,food,16,mode)
|
||||
const food=rand()<0.3?snake.step([h],dir,null,board,'wrap').snake[0]:null
|
||||
const r=snake.step(cells,dir,food,board,mode)
|
||||
if(!r.alive)break
|
||||
if(Math.abs(r.snake[0].x-h.x)>1||Math.abs(r.snake[0].y-h.y)>1)seams++
|
||||
cells=r.snake
|
||||
assert.equal(new Set(cells.map(c=>c.x+','+c.y)).size,cells.length)
|
||||
for(const c of cells)assert.ok(c.x>=0&&c.x<16&&c.y>=0&&c.y<16)
|
||||
for(const name of snake.renderPieces(cells,dir,16))assert.ok(fs.existsSync(require('node:path').join(__dirname,'../src/common/tex',name+'.png')),name)
|
||||
for(const c of cells)assert.ok(board===16 ? c.x>=0&&c.x<16&&c.y>=0&&c.y<16 : snake.ARENA.rows[c.y].includes(c.x))
|
||||
for(const name of snake.renderPieces(cells,dir,board))assert.ok(fs.existsSync(require('node:path').join(__dirname,'../src/common/tex',name+'.png')),name)
|
||||
moves++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
const assert=require('node:assert/strict')
|
||||
const c=require('../src/common/snake.js'),a=c.ARENA
|
||||
let seed=408;const random=()=>{seed=(Math.imul(seed,1664525)+1013904223)>>>0;return seed/4294967296};Math.random=random
|
||||
const dirs=[{x:1,y:0},{x:0,y:1},{x:-1,y:0},{x:0,y:-1}],names=['e','s','w','n'],opp={e:'w',w:'e',n:'s',s:'n'}
|
||||
const side=d=>names[dirs.findIndex(v=>v.x===d.x&&v.y===d.y)]
|
||||
let moves=0,seams=0
|
||||
for(const mode of ['classic','wrap'])for(let game=0;game<1000;game++){
|
||||
let model=c.initialSnake(),arrivals=model.map(()=>dirs[0]),dir=dirs[0],food=c.spawnFood(model,a)
|
||||
for(let tick=0;tick<250;tick++){
|
||||
dir=c.turn(dir,dirs[Math.floor(random()*4)])
|
||||
const result=c.step(model,dir,food,a,mode)
|
||||
if(!result.alive)break
|
||||
if(Math.abs(result.snake[0].x-model[0].x)+Math.abs(result.snake[0].y-model[0].y)>1)seams++
|
||||
model=result.snake;arrivals.unshift(dir);if(!result.ate)arrivals.pop()
|
||||
assert.equal(new Set(model.map(p=>p.x+','+p.y)).size,model.length)
|
||||
for(const p of model)assert.ok(a.rows[p.y].includes(p.x))
|
||||
const expected=model.map((p,i)=>{
|
||||
if(i===0)return 'head_'+side(dir)
|
||||
const from=side(arrivals[i-1]);if(i===model.length-1)return 'tail_'+opp[from]
|
||||
const to=opp[side(arrivals[i])]
|
||||
if((from==='e'||from==='w')&&(to==='e'||to==='w'))return 'body_h'
|
||||
if((from==='n'||from==='s')&&(to==='n'||to==='s'))return 'body_v'
|
||||
return 'corner_'+[from,to].sort().join('')
|
||||
})
|
||||
assert.deepEqual(c.renderPieces(model,dir,mode==='wrap'?a:0),expected)
|
||||
moves++
|
||||
if(result.ate){food=c.spawnFood(model,a);if(!food)break}
|
||||
}
|
||||
}
|
||||
console.log(JSON.stringify({roundMovesChecked:moves,wrapSeamsChecked:seams,historyBasedTextureClassification:'passed'}))
|
||||
@@ -0,0 +1,38 @@
|
||||
const assert = require('node:assert/strict')
|
||||
const snake = require('../src/common/snake.js')
|
||||
assert.ok(snake.ARENA, 'shared circular arena exists')
|
||||
const a = snake.ARENA
|
||||
assert.equal(a.cell,32); assert.equal(a.radius,184); assert.equal(a.offset,57)
|
||||
assert.equal(a.cells.length,89)
|
||||
const widths=[]
|
||||
for(let y=0;y<a.size;y++) {
|
||||
const row=a.cells.filter(c=>c.y===y); widths.push(row.length)
|
||||
for(const c of row) {
|
||||
const dx=Math.abs(a.offset+(c.x+.5)*a.cell-233)+a.cell/2
|
||||
const dy=Math.abs(a.offset+(c.y+.5)*a.cell-233)+a.cell/2
|
||||
assert.ok(dx*dx+dy*dy<=a.radius*a.radius,'whole tile inside circle')
|
||||
}
|
||||
}
|
||||
assert.deepEqual(widths,[3,7,9,9,11,11,11,9,9,7,3])
|
||||
assert.ok(snake.initialSnake().every(c=>a.cells.some(p=>p.x===c.x&&p.y===c.y)))
|
||||
// Exhaust every varying row/column boundary, including shortest rows.
|
||||
for(const horizontal of [true,false]) for(let line=0;line<a.size;line++) {
|
||||
const span=horizontal?a.rows[line]:a.columns[line]
|
||||
for(const sign of [-1,1]) {
|
||||
const edge=sign===1?span[span.length-1]:span[0], opposite=sign===1?span[0]:span[span.length-1]
|
||||
const c=v=>horizontal?{x:v,y:line}:{x:line,y:v}
|
||||
const dir=horizontal?{x:sign,y:0}:{x:0,y:sign}
|
||||
const cells=[c(edge),c(edge-sign),c(edge-sign*2)]
|
||||
assert.equal(snake.step(cells,dir,null,a,'classic').alive,false)
|
||||
const r=snake.step(cells,dir,null,a,'wrap')
|
||||
assert.equal(r.alive,true); assert.deepEqual(r.snake[0],c(opposite))
|
||||
assert.deepEqual(snake.renderPieces(r.snake,dir,a),[horizontal?(sign===1?'head_e':'head_w'):(sign===1?'head_s':'head_n'),horizontal?'body_h':'body_v',horizontal?(sign===1?'tail_w':'tail_e'):(sign===1?'tail_n':'tail_s')])
|
||||
}
|
||||
}
|
||||
const original=Math.random
|
||||
try {
|
||||
Math.random=()=>0
|
||||
for(const empty of a.cells) assert.deepEqual(snake.spawnFood(a.cells.filter(c=>c!==empty),a),empty)
|
||||
assert.equal(snake.spawnFood(a.cells,a),null)
|
||||
} finally {Math.random=original}
|
||||
console.log('ROUND ARENA GEOMETRY / MOVEMENT / FOOD / SEAMS PASS')
|
||||
+17
-12
@@ -40,13 +40,12 @@ test('retired timer callbacks cannot advance a resumed or restarted run', () =>
|
||||
})
|
||||
test('filling the board wins without null food crashes and retry clears the result', () => {
|
||||
const {p, timers} = page()
|
||||
// A serpentine Hamilton path, head (14,0), final food (15,0).
|
||||
const path = []
|
||||
for (let y=0;y<16;y++) for (let k=0;k<16;k++) path.push({x:y%2 ? k : 15-k,y})
|
||||
p.model = path.slice(1); p.dir = {x:1,y:0}; p.foodModel = {x:15,y:0}
|
||||
// Legal occupancy fixture isolates final-cell victory from pathfinding.
|
||||
p.model = [{x:5,y:0}, ...snake.ARENA.cells.filter(c=>!(c.y===0 && (c.x===5 || c.x===6)))]
|
||||
p.dir = {x:1,y:0}; p.foodModel = {x:6,y:0}
|
||||
p.tick(); assert.equal(p.over, true); assert.equal(p.endText, 'YOU WIN')
|
||||
assert.equal(p.scoreText, 'SCORE 253'); assert.equal(p.bestText, 'BEST 253')
|
||||
assert.equal(p.hasFood, false); assert.equal(p.snake.length, 256); assert.equal(timers.size, 0)
|
||||
assert.equal(p.scoreText, 'SCORE 86'); assert.equal(p.bestText, 'BEST 86')
|
||||
assert.equal(p.hasFood, false); assert.equal(p.snake.length, 89); assert.equal(timers.size, 0)
|
||||
p.tick(); p.onShow(); assert.equal(timers.size, 0)
|
||||
p.restart(); assert.equal(p.hasFood, true); assert.equal(p.endText, 'GAME OVER'); assert.equal(p.model.length, 3)
|
||||
assert.match(ux, /show="\{\{ hasFood \}\}"/)
|
||||
@@ -58,11 +57,11 @@ test('select modes while paused or over, resetting safely, and retry keeps mode'
|
||||
p.selectMode('wrap'); assert.equal(p.mode, 'classic', 'no mid-run mode change')
|
||||
p.onTap(); p.selectMode('garbage'); assert.equal(p.mode, 'classic')
|
||||
p.selectMode('wrap'); assert.equal(p.mode, 'wrap'); assert.equal(p.paused, true); assert.equal(timers.size, 0)
|
||||
p.onTap(); p.model = [{x:15,y:7},{x:14,y:7},{x:13,y:7}]; p.tick()
|
||||
assert.equal(p.model[0].x, 0); assert.match(p.snake[1].src, /body_h/)
|
||||
p.onTap(); p.model = [{x:9,y:7},{x:8,y:7},{x:7,y:7}]; p.tick()
|
||||
assert.equal(p.model[0].x, 1); assert.match(p.snake[1].src, /body_h/)
|
||||
p.restart(); assert.equal(p.mode, 'wrap'); assert.equal(timers.size, 1)
|
||||
p.onTap(); p.selectMode('classic'); p.onTap()
|
||||
p.model = [{x:15,y:7},{x:14,y:7},{x:13,y:7}]; p.tick()
|
||||
p.model = [{x:9,y:7},{x:8,y:7},{x:7,y:7}]; p.tick()
|
||||
assert.equal(p.over, true); assert.equal(timers.size, 0)
|
||||
p.onShow(); p.onTap(); assert.equal(p.over, true); assert.equal(timers.size, 0)
|
||||
p.restart(); assert.equal(p.over, false); assert.equal(timers.size, 1)
|
||||
@@ -95,7 +94,7 @@ test('Wrap crosses every edge, still collides with body and renders seam connect
|
||||
assert.deepEqual(snake.renderPieces([{x:0,y:7},{x:15,y:7}], dirs[0], 16), ['head_e','tail_w'])
|
||||
})
|
||||
function eatTo(p, score) {
|
||||
p.model = [{x:6,y:7}, ...Array.from({length:score+1}, (_,i) => ({x:i%16,y:12+Math.floor(i/16)}))]
|
||||
p.model = [{x:6,y:7}, ...snake.ARENA.cells.filter(c=>c.y!==7).slice(0,score+1)]
|
||||
p.dir = {x:1,y:0}; p.foodModel = {x:7,y:7}; p.tick()
|
||||
}
|
||||
test('late best read cannot lower local record; writes are serialized', () => {
|
||||
@@ -124,12 +123,18 @@ test('storage throws and corrupt values never crash or poison gameplay', () => {
|
||||
assert.equal(p.bestText, 'BEST 0'); eatTo(p, 1); assert.equal(p.bestText, 'BEST 1')
|
||||
}
|
||||
})
|
||||
test('legacy 0.7 records above the new arena maximum survive', () => {
|
||||
const writes=[]
|
||||
const {p}=page({get(o){o.success('253')},set(o){writes.push(o)}})
|
||||
eatTo(p,20); p.restart()
|
||||
assert.equal(p.bestText,'BEST 253'); assert.equal(writes.length,0)
|
||||
})
|
||||
test('best score loads raw storage value and persists only new records', () => {
|
||||
let get; const writes = []
|
||||
const {p} = page({get(o) { get=o }, set(o) { writes.push(o) }})
|
||||
assert.ok(get, 'best score requested'); assert.equal(get.default, '0')
|
||||
get.success('4'); assert.equal(p.bestText, 'BEST 4')
|
||||
p.model = [{x:6,y:7}, ...Array.from({length:6}, (_,i) => ({x:i,y:12}))]
|
||||
p.model = [{x:6,y:7}, ...snake.ARENA.cells.filter(c=>c.y!==7).slice(0,6)]
|
||||
p.foodModel = {x:7,y:7}; p.tick()
|
||||
assert.equal(p.bestText, 'BEST 5'); assert.equal(writes.length, 1)
|
||||
assert.equal(writes[0].value, '5'); assert.equal(writes[0].key, get.key)
|
||||
@@ -141,7 +146,7 @@ test('eating updates live score and accelerates to a capped 120ms', () => {
|
||||
const {p, timers} = page()
|
||||
assert.equal([...timers.values()][0].ms, 300)
|
||||
for (let score = 1; score <= 20; score++) {
|
||||
p.model = [{x:6,y:7}, ...Array.from({length:score+1}, (_,i) => ({x:i%16,y:12+Math.floor(i/16)}))]
|
||||
p.model = [{x:6,y:7}, ...snake.ARENA.cells.filter(c=>c.y!==7).slice(0,score+1)]
|
||||
p.dir = {x:1,y:0}; p.foodModel = {x:7,y:7}; p.tick()
|
||||
assert.equal(p.scoreText, 'SCORE ' + score)
|
||||
assert.equal(timers.size, 1); assert.equal([...timers.values()][0].ms, Math.max(120, 300-score*10))
|
||||
|
||||
Reference in New Issue
Block a user