A fully playable Pac-Man clone squeezed into exactly 1,024 bytes of JavaScript — including the original map layout, ghost AI, mouth animation, and food counter. Use arrow keys to play.
| Author | feiss |
|---|---|
| GitHub | github.com/feiss |
| Website | feiss.be |
| Competition | canvas |
| Year | 2019 (10th anniversary JS1K) |
| Bytes | 1024 / 1024 |
The author’s main goal was preserving the original Pac-Man map layout and gameplay feel. Ghost AI is intentionally minimal — a pseudo-random turn derived from the current direction plus a global frame counter. The final code was packed with babel-minify then regpack (a string-pattern compressor) to reach the 1024-byte limit.
c[methodName[0] + methodName[6]] — turns fillRect into fc, translate into ta, etc.(direction + frameCounter) % 4 + 1 — picks a new direction on each wall collisionpixel = origin + (target − origin) × step / 10Math.abs((frameCounter / 6) % 2 − 1.1)41 − event.which to convert arrow key codes into direction values 1–4//b.style.background = "#000";
// map
M=[g=8190, h=4226, h, h, g+1, l=4240, l,
l=8094, 130, 130, k=159, j=144, j, 8176,
j, j, k, j, j, g, h, h, 7423, j=1168,
j, l, k=4098, k, g+1, f=t=0 ];
C=[];
// canvas trick
for(i in c) c[i[0]+i[6]] = c[i]
for(i in M) {
// convert map numbers to binary
m = M[i].toString(2).padStart(13,0);
// duplicate and flip to build right half
for(j in m) m+=m[m.length-j*2-1];
M[i] = m.split('');
C[i] = []
}
F = (x,y,w,h,p)=>{ c.fillStyle='#'+p;c.fc(x,y,w,h,p) }
N = r=>{
//fill all in blue
F(0,0,l,l,'009')
for(i in M) for(j in M[i]){
+M[i][j] && F(8+j*16, 8+i*16, 30, 30,111);
if (r) {
// reset map
C[i][j] = +M[i][j];
g=h=>({x:h,y:h,z:h,w:h,Z:h,W:h,d:(t++%4)+1,s:9})
// 4 ghosts and pacman
G=[g(25),g(5),g(0),g(17),g(16)]
k=4
}
}
}
N(1)
c.lineWidth= 12;
onkeydown=e=>k=41-e.which; // <4 ^3 >2 v1 .0
setInterval(()=>{
// draw map
N()
// draw food
S = 298;
for(i in M) for(j in M[i]){
// if there's food in this cell, paint it and decrement counter
C[i][j] && F(22+j*16, 22+i*16, 3, 3, 'eee', S--);
}
// draw score
c.fx(S,220,220);
for(i in G) with(G[i]){
if (s>9){
x = z;
y = w;
z -= d%2?0:d-3; // advance x
w -= d%2?d-2:0; // advance y
s = 0 // restart microstep counter
}
// pacman turning in intersections
if (i == 4 && d != k){
Z = x - (k%2?0:k-3);
W = y - (k%2?k-2:0);
if(+M[W][Z]){
d = k;
z = Z;
w = W;
s = 0
}
}
if(w<0 || !+M[w][z]){
z = x; w = y; // restore prev position
d = i==4?k:(d+t)%4+1; // pseudo-random turn. This is all the AI there is :P
s = 7
}
if(i<4) {
// game over
if (z==G[4].x && w==G[4].y) N(1)
// draw ghosts
c.ta(X=12 + (x + (z - x)*s/10) * 16, Y=10 + (y + (w - y)*s/10) * 16);
//select color
h=['f77','c70','d22','09c'][i]
// body
F(1,5,22,21,h);
F(4,2,16,8,h);
F(6,23,13,3,111); // gap between legs
F(4,8,16,7,'FFF'); //white of eyes
F(7,8,11,4,111); // pupils
F(10,0,4,26,h); // top of head + eyes separation + middle leg
c.ta(-X,-Y);
}
if(i==4) { // draw pacman
// eat
C[y][x] = 0;
f = d%2?d/2:d%3;
m = (t++/6)%2-1.1; // mouth animation
if(m<0) m=-m; // abs
c.strokeStyle='#ff0';
c.ba();
c.arc(22 + (x + (z - x)*s/10) * 16, 22 + (y + (w - y)*s/10) * 16, 5, 3.1*f+m, 3.1*f-m);
c.stroke();
}
s++;
}
}, 22)
var methodName
for (methodName in c) {
c[methodName[0] + methodName[6]] = c[methodName]
}
var mazeRows = [
8190, 4226, 4226, 4226, 8191, 4240, 4240,
8094, 130, 130, 159, 144, 144, 8176,
144, 144, 159, 144, 144, 8190, 4226,
4226, 7423, 1168, 1168, 8094, 4098, 4098,
8191
]
var foodGrid = []
var frameCounter = 0
var facingAngle = 0
var entities
var playerDirection
;(function buildMaze() {
var row, col, halfRow
for (row in mazeRows) {
halfRow = mazeRows[row].toString(2).padStart(13, 0)
for (col in halfRow) {
halfRow += halfRow[halfRow.length - col * 2 - 1]
}
mazeRows[row] = halfRow.split('')
foodGrid[row] = []
}
}())
function fillBlock(x, y, w, h, color) {
c.fillStyle = '#' + color
c.fc(x, y, w, h)
}
function makeEntity(startPos) {
return {
col: startPos,
row: startPos,
nextCol: startPos,
nextRow: startPos,
turnCol: startPos,
turnRow: startPos,
direction: (frameCounter++ % 4) + 1,
step: 9
}
}
function drawScene(resetGame) {
var row, col
fillBlock(0, 0, 500, 500, '009')
for (row in mazeRows) {
for (col in mazeRows[row]) {
if (+mazeRows[row][col]) {
fillBlock(8 + col * 16, 8 + row * 16, 30, 30, '111')
}
if (resetGame) {
foodGrid[row][col] = +mazeRows[row][col]
}
}
}
if (resetGame) {
entities = [
makeEntity(25),
makeEntity(5),
makeEntity(0),
makeEntity(17),
makeEntity(16)
]
playerDirection = 4
}
}
drawScene(true)
c.lineWidth = 12
onkeydown = function handleKey(event) {
playerDirection = 41 - event.which
}
function gameTick() {
var row, col, i, entity, ghostColor, pixelX, pixelY, mouthAngle
drawScene()
var foodRemaining = 298
for (row in mazeRows) {
for (col in mazeRows[row]) {
if (foodGrid[row][col]) {
fillBlock(22 + col * 16, 22 + row * 16, 3, 3, 'eee')
foodRemaining -= 1
}
}
}
c.fx(foodRemaining, 220, 220)
for (i in entities) {
entity = entities[i]
if (entity.step > 9) {
entity.col = entity.nextCol
entity.row = entity.nextRow
entity.nextCol -= (entity.direction % 2) ? 0 : entity.direction - 3
entity.nextRow -= (entity.direction % 2) ? entity.direction - 2 : 0
entity.step = 0
}
if (i == 4 && entity.direction !== playerDirection) {
entity.turnCol = entity.col - ((playerDirection % 2) ? 0 : playerDirection - 3)
entity.turnRow = entity.row - ((playerDirection % 2) ? playerDirection - 2 : 0)
if (+mazeRows[entity.turnRow][entity.turnCol]) {
entity.direction = playerDirection
entity.nextCol = entity.turnCol
entity.nextRow = entity.turnRow
entity.step = 0
}
}
if (entity.nextRow < 0 || entity.nextRow >= mazeRows.length || !+mazeRows[entity.nextRow][entity.nextCol]) {
entity.nextCol = entity.col
entity.nextRow = entity.row
entity.direction = (i == 4) ? playerDirection : (entity.direction + frameCounter) % 4 + 1
entity.step = 7
}
if (i < 4) {
if (entity.nextCol === entities[4].col && entity.nextRow === entities[4].row) {
drawScene(true)
}
pixelX = 12 + (entity.col + (entity.nextCol - entity.col) * entity.step / 10) * 16
pixelY = 10 + (entity.row + (entity.nextRow - entity.row) * entity.step / 10) * 16
c.ta(pixelX, pixelY)
ghostColor = ['f77', 'c70', 'd22', '09c'][i]
fillBlock( 1, 5, 22, 21, ghostColor)
fillBlock( 4, 2, 16, 8, ghostColor)
fillBlock( 6, 23, 13, 3, '111')
fillBlock( 4, 8, 16, 7, 'FFF')
fillBlock( 7, 8, 11, 4, '111')
fillBlock(10, 0, 4, 26, ghostColor)
c.ta(-pixelX, -pixelY)
}
if (i == 4) {
foodGrid[entity.row][entity.col] = 0
facingAngle = (entity.direction % 2) ? entity.direction / 2 : entity.direction % 3
mouthAngle = (frameCounter++ / 6) % 2 - 1.1
if (mouthAngle < 0) {
mouthAngle = -mouthAngle
}
pixelX = 22 + (entity.col + (entity.nextCol - entity.col) * entity.step / 10) * 16
pixelY = 22 + (entity.row + (entity.nextRow - entity.row) * entity.step / 10) * 16
c.strokeStyle = '#ff0'
c.ba()
c.arc(pixelX, pixelY, 5, 3.1 * facingAngle + mouthAngle, 3.1 * facingAngle - mouthAngle)
c.stroke()
}
entity.step += 1
}
}
setInterval(gameTick, 22)