v0.6: real PNG textures (head with eyes, scaled body, corners, tapering tails, apple, board); renderPieces classifier + tests
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
# Bake snake textures. Working resolution 80px, saved at 20px (cell size).
|
||||
# Base tiles: body, head (eyes), apple, plus rotated variants written into src/common/tex/.
|
||||
import os
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
TILE = 80
|
||||
OUT = os.path.expanduser('~/vela-snake/src/common/tex')
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
|
||||
def rr(d, box, rad, fill):
|
||||
d.rounded_rectangle(box, radius=rad, fill=fill)
|
||||
|
||||
def body():
|
||||
W = TILE
|
||||
img = Image.new('RGBA', (W, W), (0, 0, 0, 0))
|
||||
d = ImageDraw.Draw(img)
|
||||
rr(d, (0, 10, W, W - 10), 14, (60, 183, 90, 255))
|
||||
for cx, cy in ((18, 26), (W - 30, W - 38)):
|
||||
d.ellipse((cx - 14, cy - 14, cx + 14, cy + 14), fill=(46, 160, 74, 255))
|
||||
d.ellipse((cx - 6, cy - 6, cx + 6, cy + 6), fill=(60, 183, 90, 255))
|
||||
return img
|
||||
|
||||
def head():
|
||||
W = TILE
|
||||
img = Image.new('RGBA', (W, W), (0, 0, 0, 0))
|
||||
d = ImageDraw.Draw(img)
|
||||
rr(d, (10, 12, W, W - 12), 22, (64, 188, 110, 255))
|
||||
d.ellipse((W - 52, 24, W - 32, 44), fill=(255, 255, 255, 255))
|
||||
d.ellipse((W - 46, 30, W - 38, 38), fill=(30, 30, 30, 255))
|
||||
return img
|
||||
|
||||
def apple():
|
||||
W = TILE
|
||||
img = Image.new('RGBA', (W, W), (0, 0, 0, 0))
|
||||
d = ImageDraw.Draw(img)
|
||||
d.ellipse((8, 14, W - 8, W - 6), fill=(224, 62, 62, 255))
|
||||
d.ellipse((16, 24, 34, 42), fill=(255, 255, 255, 70))
|
||||
d.polygon(((W // 2 - 4, 12), (W // 2 + 18, 4), (W // 2 + 10, 16)), fill=(56, 128, 56, 255))
|
||||
d.rectangle((W // 2 - 3, 10, W // 2 + 1, 18), fill=(90, 60, 30, 255))
|
||||
return img
|
||||
|
||||
def head_open():
|
||||
W = TILE
|
||||
img = head()
|
||||
d = ImageDraw.Draw(img)
|
||||
d.rectangle((0, 0, 10, W), fill=(0, 0, 0, 0))
|
||||
d.rectangle((10, 36, 34, 44), fill=(226, 62, 62, 255))
|
||||
d.polygon(((10, 36), (26, 28), (30, 40)), fill=(226, 62, 62, 255))
|
||||
return img
|
||||
|
||||
def save20(img, name):
|
||||
img.resize((20, 20), Image.LANCZOS).save(os.path.join(OUT, name))
|
||||
|
||||
# base tiles: body + head drawn mouth-E (E = right); rotated variants cover all directions
|
||||
b = body()
|
||||
save20(b, 'body.png') # 4-way base body tile
|
||||
h = head_open() # head with tongue + eyes, W edge open
|
||||
save20(h, 'head_e.png') # facing east
|
||||
save20(h.transpose(Image.FLIP_LEFT_RIGHT), 'head_w.png')
|
||||
save20(h.transpose(Image.ROTATE_90), 'head_n.png') # E->N under ccw
|
||||
save20(h.transpose(Image.ROTATE_90).transpose(Image.FLIP_TOP_BOTTOM), 'head_s.png')
|
||||
save20(apple(), 'apple.png')
|
||||
print('base tiles done')
|
||||
+9
-5
@@ -25,10 +25,11 @@ function mk() {
|
||||
let c = mk();
|
||||
assert.equal(c.model.length, 3, 'initialized to length 3');
|
||||
assert.equal(c.snake.length, 3, 'rendered 3 segments');
|
||||
assert.equal(c.snake[0].cls, 'seg head', 'head flagged');
|
||||
assert.equal(c.snake[1].cls, 'seg', 'body flagged');
|
||||
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 + 1) + 'px', 'head x mapped to px');
|
||||
assert.equal(c.snake[0].left, (73 + 6 * 20) + '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');
|
||||
@@ -39,6 +40,7 @@ c.tick();
|
||||
assert.equal(c.dir.x, 0, 'dir x');
|
||||
assert.equal(c.dir.y, 1, 'direction turned down');
|
||||
assert.equal(c.model[0].y, 8, 'head moved down');
|
||||
assert.match(c.snake[0].src, /head_s\.png$/, 'head faces south after turn');
|
||||
|
||||
c.onSwipe({ direction: 'up' });
|
||||
c.tick();
|
||||
@@ -57,11 +59,13 @@ assert.equal(c.over, false);
|
||||
assert.equal(c.model.length, 3);
|
||||
assert.equal(c.scoreText, 'SCORE 0');
|
||||
|
||||
// template uses documented directives/events, no transforms, no crown remnants
|
||||
// template uses image components for textures + documented directives/events
|
||||
assert.match(ux, /<image class="board" src="\/common\/tex\/board\.png">/);
|
||||
assert.match(ux, /<image class="seg" src="\{\{ seg\.src \}\}"/);
|
||||
assert.match(ux, /for="\{\{ seg in snake \}\}"/);
|
||||
assert.match(ux, /show="\{\{ over \}\}"/);
|
||||
assert.match(ux, /onswipe="onSwipe"/);
|
||||
assert.doesNotMatch(ux, /crownrotationchanged/i);
|
||||
assert.doesNotMatch(ux, /transform\s*:/);
|
||||
|
||||
console.log('PAGE WIRING PASS (movement, turn, reversal, game over, restart, directives)');
|
||||
console.log('PAGE WIRING PASS (textures, movement, turn, reversal, game over, restart, directives)');
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// TDD: piece classification for textured rendering.
|
||||
// renderPieces(snake, dir) -> array of texture names, one per segment (head first).
|
||||
const assert = require('assert/strict')
|
||||
const fs = require('fs')
|
||||
const vm = require('vm')
|
||||
|
||||
const src = fs.readFileSync(__dirname + '/../src/common/snake.js', 'utf8')
|
||||
const sandbox = { module: { exports: {} } }
|
||||
vm.runInNewContext(src, sandbox)
|
||||
const snake = sandbox.module.exports
|
||||
|
||||
// compass sides: e/w/n/s (screen coords, y grows downward)
|
||||
function pieces(list, dir) { return snake.renderPieces(list, dir) }
|
||||
|
||||
// -- initial snake: head east, horizontal body, tail pointing west
|
||||
assert.deepEqual(
|
||||
pieces([{ x: 6, y: 7 }, { x: 5, y: 7 }, { x: 4, y: 7 }], { x: 1, y: 0 }),
|
||||
['head_e', 'body_h', 'tail_w'])
|
||||
|
||||
// -- vertical snake moving south
|
||||
assert.deepEqual(
|
||||
pieces([{ x: 3, y: 5 }, { x: 3, y: 4 }, { x: 3, y: 4 + 1 }], { x: 0, y: 1 })
|
||||
.map((p, i) => i), // shape check below with explicit list instead
|
||||
[0, 1, 2])
|
||||
assert.deepEqual(
|
||||
pieces([{ x: 3, y: 5 }, { x: 3, y: 6 }, { x: 3, y: 7 }], { x: 0, y: 1 }),
|
||||
['head_s', 'body_v', 'tail_s'])
|
||||
|
||||
// -- corner: head turned from east to south
|
||||
// snake [(6,8),(6,7),(5,7)]: neck connects south (to head) and west (to rest);
|
||||
// tail cell (5,7)'s neighbor is east, so its tip points west
|
||||
assert.deepEqual(
|
||||
pieces([{ x: 6, y: 8 }, { x: 6, y: 7 }, { x: 5, y: 7 }], { x: 0, y: 1 }),
|
||||
['head_s', 'corner_sw', 'tail_w'])
|
||||
|
||||
// -- corner: head turned from east to north
|
||||
assert.deepEqual(
|
||||
pieces([{ x: 6, y: 6 }, { x: 6, y: 7 }, { x: 5, y: 7 }], { x: 0, y: -1 }),
|
||||
['head_n', 'corner_nw', 'tail_w'])
|
||||
|
||||
// -- corner heading west turning south (es corner mirrored)
|
||||
// tail cell (5,7)'s neighbor is west, so its tip points east
|
||||
assert.deepEqual(
|
||||
pieces([{ x: 4, y: 8 }, { x: 4, y: 7 }, { x: 5, y: 7 }], { x: -1, y: 0 }),
|
||||
['head_w', 'corner_es', 'tail_e'])
|
||||
|
||||
// -- double corner (S shape): head moving south, west, then north —
|
||||
// hand-verified: every middle cell of this tight zigzag is a corner
|
||||
// [(6,8) head][ (6,7) sw ][ (5,7) en ][ (5,6) sw ][ (4,6) en ][ (4,5) tail ]
|
||||
assert.deepEqual(
|
||||
pieces([{ x: 6, y: 8 }, { x: 6, y: 7 }, { x: 5, y: 7 }, { x: 5, y: 6 }, { x: 4, y: 6 }, { x: 4, y: 5 }], { x: 0, y: 1 }),
|
||||
['head_s', 'corner_sw', 'corner_en', 'corner_sw', 'corner_en', 'tail_n'])
|
||||
|
||||
// -- every returned piece maps to a texture that must exist
|
||||
const TEX = ['head_e', 'head_w', 'head_n', 'head_s',
|
||||
'body_h', 'body_v',
|
||||
'corner_en', 'corner_es', 'corner_nw', 'corner_sw',
|
||||
'tail_e', 'tail_w', 'tail_n', 'tail_s']
|
||||
const dirTex = __dirname + '/../src/common/tex'
|
||||
for (const p of TEX) {
|
||||
assert.ok(fs.existsSync(dirTex + '/' + p + '.png'), 'missing texture ' + p)
|
||||
}
|
||||
|
||||
// -- food + board textures
|
||||
assert.ok(fs.existsSync(dirTex + '/apple.png'), 'missing apple texture')
|
||||
assert.ok(fs.existsSync(dirTex + '/board.png'), 'missing board texture')
|
||||
|
||||
console.log('PIECE CLASSIFICATION PASS')
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Reference in New Issue
Block a user