refactor DotDude: standardize grid to 10x15 and overhaul movement logic

- Reduce playfield from 20x15 to 10x15 tiles (60x60px) and center in canvas
- Replace raw pixel math with tile-based coordinate helpers (`_px`, `_py`, `_col`, `_row`)
- Rewrite player movement to use hard-snap alignment and tile-center turning thresholds
- Switch ghosts to tile-target-based navigation with `_pickNextTarget` and distance checks
- Update layout to a symmetric 10-column maze with ghost house and corridors
- Redraw HUD and background for the new layout dimensions
This commit is contained in:
Brian Fertig 2026-05-09 12:48:26 -06:00
parent b6002fed00
commit 2aabbb158a
1 changed files with 226 additions and 181 deletions

View File

@ -1,23 +1,28 @@
const LAYOUT = [ // 10 cols × 15 rows, 60 × 60 px tiles
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], // Playfield is 600 × 900 px, centered at x = 500..1100 in the 1600 × 900 canvas
[1,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,1], const COLS = 10;
[1,0,1,1,0,1,0,1,1,0,0,1,1,0,1,0,1,1,0,1],
[1,0,1,1,0,0,0,1,1,0,0,1,1,0,0,0,1,1,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,1,1,0,1,1,0,1,1,1,1,0,1,1,0,1,1,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,1,1,0,1,1,0,1,1,1,1,0,1,1,0,1,1,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,1,1,0,1,1,0,1,1,1,1,0,1,1,0,1,1,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,1,1,0,1,1,0,1,1,1,1,0,1,1,0,1,1,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,1,1,0,0,0,1,1,0,0,1,1,0,0,0,1,1,0,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
];
const ROWS = 15; const ROWS = 15;
const COLS = 20; const TW = 60;
const TH = 60;
const OX = (1600 - COLS * TW) / 2; // left edge of maze = 500
const LAYOUT = [
[1,1,1,1,1,1,1,1,1,1], // 0 top wall
[1,0,0,0,0,0,0,0,0,1], // 1 open corridor
[1,0,1,1,0,0,1,1,0,1], // 2 block rooms
[1,0,1,1,0,0,1,1,0,1], // 3 (thick walls, same as row 2)
[1,0,0,0,0,0,0,0,0,1], // 4 open corridor
[1,0,1,0,1,1,0,1,0,1], // 5 pre-ghost-house
[1,0,1,0,0,0,0,1,0,1], // 6 ghost house (open centre)
[1,0,1,0,1,1,0,1,0,1], // 7 post-ghost-house (mirror of 5)
[1,0,0,0,0,0,0,0,0,1], // 8 open corridor
[1,0,1,1,0,0,1,1,0,1], // 9 block rooms (mirror of 2)
[1,0,1,1,0,0,1,1,0,1], // 10 (mirror of 3)
[1,0,0,0,0,0,0,0,0,1], // 11 open corridor
[1,1,0,1,0,0,1,0,1,1], // 12 narrow passage
[1,0,0,0,0,0,0,0,0,1], // 13 open corridor (player spawn)
[1,1,1,1,1,1,1,1,1,1], // 14 bottom wall
];
export default class DotDude extends Phaser.Scene { export default class DotDude extends Phaser.Scene {
constructor() { super({ key: 'DotDude' }); } constructor() { super({ key: 'DotDude' }); }
@ -33,17 +38,16 @@ export default class DotDude extends Phaser.Scene {
create() { create() {
this.cfg = this.cache.json.get('ddConfig'); this.cfg = this.cache.json.get('ddConfig');
this.TW = 80;
this.TH = 60;
// Dots grid: copy of open cells this.dots = LAYOUT.map(row => row.map(cell => (cell === 0 ? 1 : 0)));
this.dots = LAYOUT.map(row => row.map(cell => cell === 0 ? 1 : 0));
this.totalDots = this.dots.flat().filter(d => d === 1).length; this.totalDots = this.dots.flat().filter(d => d === 1).length;
this.dotsEaten = 0; this.dotsEaten = 0;
this._initPlayer(); this._initPlayer();
this._initGhosts(); this._initGhosts();
// Background must be added first so it sits below all other layers
this._drawBackground();
this.mazeGfx = this.add.graphics(); this.mazeGfx = this.add.graphics();
this.mainGfx = this.add.graphics(); this.mainGfx = this.add.graphics();
this.gameActive = true; this.gameActive = true;
@ -55,128 +59,119 @@ export default class DotDude extends Phaser.Scene {
this._setupInput(); this._setupInput();
} }
_initPlayer() { // ── helpers ─────────────────────────────────────────────────────────────
const startCol = 10, startRow = 12;
this.player = {
x: (startCol + 0.5) * 80,
y: (startRow + 0.5) * 60,
dir: { dx: 0, dy: 0 },
wantDir: { dx: 1, dy: 0 },
};
}
_initGhosts() { _px(col) { return OX + (col + 0.5) * TW; } // tile col → pixel x centre
const ghostCols = [7, 10, 13]; _py(row) { return (row + 0.5) * TH; } // tile row → pixel y centre
const ghostRow = 6; _col(px) { return Math.floor((px - OX) / TW); }
const colors = [0xff4444, 0xff88ff, 0x44ffff]; _row(py) { return Math.floor(py / TH); }
this.ghosts = ghostCols.map((col, i) => ({
x: (col + 0.5) * 80,
y: (ghostRow + 0.5) * 60,
dir: { dx: i === 1 ? -1 : 1, dy: 0 },
color: colors[i],
active: false,
}));
this.cfg.ghostStartDelay.forEach((delay, i) => {
this.time.delayedCall(delay, () => {
if (this.gameActive && this.ghosts[i]) this.ghosts[i].active = true;
});
});
}
_buildHUD() {
this.livesText = this.add.text(800, 6, '♥ '.repeat(this.globalLives).trim(), {
fontSize: '20px', fontFamily: 'monospace', color: '#ff4444',
}).setOrigin(0.5, 0).setDepth(10);
this.dotsText = this.add.text(1580, 6, `DOTS: ${this.totalDots}`, {
fontSize: '20px', fontFamily: 'monospace', color: '#ffff00',
}).setOrigin(1, 0).setDepth(10);
this.add.text(20, 6, 'DOT DUDE', {
fontSize: '20px', fontFamily: 'monospace', color: '#ffaa00',
}).setDepth(10);
}
_setupInput() {
this.cursors = this.input.keyboard.createCursorKeys();
}
_wallAt(col, row) { _wallAt(col, row) {
if (col < 0 || col >= COLS || row < 0 || row >= ROWS) return true; if (col < 0 || col >= COLS || row < 0 || row >= ROWS) return true;
return LAYOUT[row][col] === 1; return LAYOUT[row][col] === 1;
} }
_canMoveTo(px, py) { _canMoveTo(px, py, r) {
const r = 15; return [
const tileOf = (x, y) => ({ [px - r + 1, py - r + 1],
col: Math.floor(x / this.TW), [px + r - 1, py - r + 1],
row: Math.floor(y / this.TH), [px - r + 1, py + r - 1],
}); [px + r - 1, py + r - 1],
const corners = [ ].every(([x, y]) => !this._wallAt(this._col(x), this._row(y)));
tileOf(px - r + 1, py - r + 1),
tileOf(px + r - 1, py - r + 1),
tileOf(px - r + 1, py + r - 1),
tileOf(px + r - 1, py + r - 1),
];
return corners.every(({ col, row }) => !this._wallAt(col, row));
} }
_canGhostMoveTo(px, py) { // ── init ─────────────────────────────────────────────────────────────────
const r = 13;
const tileOf = (x, y) => ({ _initPlayer() {
col: Math.floor(x / this.TW), this.player = {
row: Math.floor(y / this.TH), x: this._px(4), y: this._py(13),
}); dir: { dx: 0, dy: 0 },
const corners = [ wantDir: { dx: 1, dy: 0 },
tileOf(px - r + 1, py - r + 1), };
tileOf(px + r - 1, py - r + 1),
tileOf(px - r + 1, py + r - 1),
tileOf(px + r - 1, py + r - 1),
];
return corners.every(({ col, row }) => !this._wallAt(col, row));
} }
_initGhosts() {
const defs = [
{ col: 3, row: 6, dir: { dx: -1, dy: 0 }, color: 0xff4444 },
{ col: 5, row: 6, dir: { dx: 1, dy: 0 }, color: 0xff88ff },
{ col: 6, row: 6, dir: { dx: 1, dy: 0 }, color: 0x44ffff },
];
this.ghosts = defs.map(g => ({
x: this._px(g.col), y: this._py(g.row),
targetX: this._px(g.col), targetY: this._py(g.row),
dir: g.dir, color: g.color, active: false,
}));
this.cfg.ghostStartDelay.forEach((ms, i) => {
this.time.delayedCall(ms, () => {
if (!this.gameActive || !this.ghosts[i]) return;
const ghost = this.ghosts[i];
ghost.active = true;
this._pickNextTarget(ghost);
});
});
}
_pickNextTarget(ghost) {
const col = this._col(ghost.x);
const row = this._row(ghost.y);
const dir = this._chooseGhostDir(ghost, col, row, false);
ghost.dir = dir;
if (dir.dx !== 0 || dir.dy !== 0) {
ghost.targetX = this._px(col + dir.dx);
ghost.targetY = this._py(row + dir.dy);
}
}
_buildHUD() {
// Left panel
this.add.text(250, 80, 'DOT DUDE', {
fontSize: '30px', fontFamily: 'monospace', color: '#ffaa00',
}).setOrigin(0.5).setDepth(10);
this.livesText = this.add.text(250, 160, '♥ '.repeat(this.globalLives).trim(), {
fontSize: '30px', fontFamily: 'monospace', color: '#ff4444',
}).setOrigin(0.5).setDepth(10);
// Right panel
this.add.text(1350, 80, 'DOTS', {
fontSize: '26px', fontFamily: 'monospace', color: '#aaaaaa',
}).setOrigin(0.5).setDepth(10);
this.dotsText = this.add.text(1350, 140, `${this.totalDots}`, {
fontSize: '48px', fontFamily: 'monospace', color: '#ffff00',
}).setOrigin(0.5).setDepth(10);
}
_setupInput() {
this.cursors = this.input.keyboard.createCursorKeys();
}
// ── update ───────────────────────────────────────────────────────────────
update(time, delta) { update(time, delta) {
if (!this.gameActive) return; if (!this.gameActive) return;
const dt = delta / 1000; const dt = delta / 1000;
// Input // Input → buffer wantDir
if (this.cursors.left.isDown) this.player.wantDir = { dx: -1, dy: 0 }; if (this.cursors.left.isDown) this.player.wantDir = { dx: -1, dy: 0 };
else if (this.cursors.right.isDown) this.player.wantDir = { dx: 1, dy: 0 }; else if (this.cursors.right.isDown) this.player.wantDir = { dx: 1, dy: 0 };
else if (this.cursors.up.isDown) this.player.wantDir = { dx: 0, dy: -1 }; else if (this.cursors.up.isDown) this.player.wantDir = { dx: 0, dy: -1 };
else if (this.cursors.down.isDown) this.player.wantDir = { dx: 0, dy: 1 }; else if (this.cursors.down.isDown) this.player.wantDir = { dx: 0, dy: 1 };
const speed = this.cfg.playerSpeed; this._movePlayer(dt);
const { wantDir, dir } = this.player;
// Try wanted direction // Eat dot at current tile
const wx = this.player.x + wantDir.dx * speed * dt; const col = this._col(this.player.x);
const wy = this.player.y + wantDir.dy * speed * dt; const row = this._row(this.player.y);
if (this._canMoveTo(wx, wy)) { if (col >= 0 && col < COLS && row >= 0 && row < ROWS && this.dots[row][col] === 1) {
this.player.dir = { ...wantDir };
this.player.x = wx;
this.player.y = wy;
} else {
// Keep current direction
const dx2 = this.player.x + dir.dx * speed * dt;
const dy2 = this.player.y + dir.dy * speed * dt;
if (this._canMoveTo(dx2, dy2)) {
this.player.x = dx2;
this.player.y = dy2;
}
}
// Eat dot
const col = Math.floor(this.player.x / this.TW);
const row = Math.floor(this.player.y / this.TH);
if (row >= 0 && row < ROWS && col >= 0 && col < COLS && this.dots[row][col] === 1) {
this.dots[row][col] = 0; this.dots[row][col] = 0;
this.dotsEaten++; this.dotsEaten++;
this.dotsText.setText(`DOTS: ${this.totalDots - this.dotsEaten}`); this.dotsText.setText(`${this.totalDots - this.dotsEaten}`);
} }
// Update ghosts // Ghosts
this.ghosts.forEach(g => this._updateGhost(g, dt)); this.ghosts.forEach(g => this._updateGhost(g, dt));
// Ghost collision
for (const g of this.ghosts) { for (const g of this.ghosts) {
if (g.active && Math.hypot(g.x - this.player.x, g.y - this.player.y) < 26) { if (g.active && Math.hypot(g.x - this.player.x, g.y - this.player.y) < 26) {
this._endGame(false); this._endGame(false);
@ -184,11 +179,7 @@ export default class DotDude extends Phaser.Scene {
} }
} }
// Win check if (this.dotsEaten >= this.totalDots) { this._endGame(true); return; }
if (this.dotsEaten >= this.totalDots) {
this._endGame(true);
return;
}
// Mouth animation // Mouth animation
this.mouthAngle += this.mouthDir * 3 * dt; this.mouthAngle += this.mouthDir * 3 * dt;
@ -198,68 +189,133 @@ export default class DotDude extends Phaser.Scene {
this._drawDynamic(); this._drawDynamic();
} }
_movePlayer(dt) {
const p = this.player;
const move = this.cfg.playerSpeed * dt;
// Tile the player's centre currently occupies
const col = this._col(p.x);
const row = this._row(p.y);
const cx = this._px(col);
const cy = this._py(row);
// Allow a direction change from anywhere within the current tile (half-tile threshold).
// This is forgiving: no pixel-perfect timing needed.
const { dx: wdx, dy: wdy } = p.wantDir;
if (Math.abs(p.x - cx) < TW / 2 && Math.abs(p.y - cy) < TH / 2) {
if (!this._wallAt(col + wdx, row + wdy)) {
p.dir = { dx: wdx, dy: wdy };
}
}
const { dx, dy } = p.dir;
if (dx !== 0) {
// ── Horizontal movement ──────────────────────────────────────────
// Hard-snap Y to the row's lane centre every frame.
p.y = cy;
const nx = p.x + dx * move;
const nCol = this._col(nx);
if (nCol !== col && this._wallAt(nCol, row)) {
// About to enter a wall column: park at the near edge of the open tile.
p.x = dx > 0 ? OX + (col + 1) * TW - 1 : OX + col * TW + 1;
p.dir = { dx: 0, dy: 0 };
} else {
p.x = nx;
}
} else if (dy !== 0) {
// ── Vertical movement ────────────────────────────────────────────
// Hard-snap X to the column's lane centre every frame.
p.x = cx;
const ny = p.y + dy * move;
const nRow = this._row(ny);
if (nRow !== row && this._wallAt(col, nRow)) {
p.y = dy > 0 ? (row + 1) * TH - 1 : row * TH + 1;
p.dir = { dx: 0, dy: 0 };
} else {
p.y = ny;
}
} else {
// Stopped: snap to tile centre so the turn threshold works correctly.
p.x = cx;
p.y = cy;
}
}
_updateGhost(ghost, dt) { _updateGhost(ghost, dt) {
if (!ghost.active) return; if (!ghost.active) return;
const speed = this.cfg.ghostSpeed; const move = this.cfg.ghostSpeed * dt;
const { dir } = ghost;
const nx = ghost.x + dir.dx * speed * dt; const tdx = ghost.targetX - ghost.x;
const ny = ghost.y + dir.dy * speed * dt; const tdy = ghost.targetY - ghost.y;
const dist = Math.hypot(tdx, tdy);
if (this._canGhostMoveTo(nx, ny)) { if (dist <= move) {
ghost.x = nx; // Arrived at the target tile centre: snap and choose next tile.
ghost.y = ny; ghost.x = ghost.targetX;
} ghost.y = ghost.targetY;
this._pickNextTarget(ghost);
// Near tile center: rechoose direction } else {
const col = Math.floor(ghost.x / this.TW); ghost.x += (tdx / dist) * move;
const row = Math.floor(ghost.y / this.TH); ghost.y += (tdy / dist) * move;
const cx = (col + 0.5) * this.TW;
const cy = (row + 0.5) * this.TH;
const snapD = speed * dt * 4;
if (Math.abs(ghost.x - cx) < snapD && Math.abs(ghost.y - cy) < snapD) {
ghost.x = cx;
ghost.y = cy;
ghost.dir = this._chooseGhostDir(ghost, col, row);
} }
} }
_chooseGhostDir(ghost, col, row) { // allowReverse: true when the ghost is wall-blocked and needs an escape route.
_chooseGhostDir(ghost, col, row, allowReverse) {
const dirs = [{ dx: 1, dy: 0 }, { dx: -1, dy: 0 }, { dx: 0, dy: 1 }, { dx: 0, dy: -1 }]; const dirs = [{ dx: 1, dy: 0 }, { dx: -1, dy: 0 }, { dx: 0, dy: 1 }, { dx: 0, dy: -1 }];
const reverse = { dx: -ghost.dir.dx, dy: -ghost.dir.dy }; const rev = { dx: -ghost.dir.dx, dy: -ghost.dir.dy };
const pCol = Math.floor(this.player.x / this.TW); const pc = this._col(this.player.x);
const pRow = Math.floor(this.player.y / this.TH); const pr = this._row(this.player.y);
let best = null, bestScore = Infinity; let best = null, bestScore = Infinity;
for (const d of dirs) { for (const d of dirs) {
if (d.dx === reverse.dx && d.dy === reverse.dy) continue; if (!allowReverse && d.dx === rev.dx && d.dy === rev.dy) continue;
const nc = col + d.dx, nr = row + d.dy; const nc = col + d.dx, nr = row + d.dy;
if (this._wallAt(nc, nr)) continue; if (this._wallAt(nc, nr)) continue;
const score = Math.abs(nc - pCol) + Math.abs(nr - pRow); const score = Math.abs(nc - pc) + Math.abs(nr - pr);
if (score < bestScore) { bestScore = score; best = d; } if (score < bestScore) { bestScore = score; best = d; }
} }
return best || reverse; // Last resort: try reverse even if not explicitly allowed
if (!best && !this._wallAt(col + rev.dx, row + rev.dy)) best = rev;
return best || { dx: 0, dy: 0 };
}
// ── drawing ──────────────────────────────────────────────────────────────
_drawBackground() {
const bg = this.add.graphics();
// Full canvas
bg.fillStyle(0x000011);
bg.fillRect(0, 0, 1600, 900);
// Side panels slightly lighter
bg.fillStyle(0x08081a);
bg.fillRect(0, 0, OX, 900);
bg.fillRect(OX + COLS * TW, 0, OX, 900);
// Subtle panel separators
bg.lineStyle(1, 0x222244, 0.8);
bg.lineBetween(OX, 0, OX, 900);
bg.lineBetween(OX + COLS * TW, 0, OX + COLS * TW, 900);
} }
_drawMaze() { _drawMaze() {
const g = this.mazeGfx; const g = this.mazeGfx;
g.clear();
// Background
g.fillStyle(0x000022); g.fillStyle(0x000022);
g.fillRect(0, 0, 1600, 900); g.fillRect(OX, 0, COLS * TW, ROWS * TH);
// Walls
for (let r = 0; r < ROWS; r++) { for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) { for (let c = 0; c < COLS; c++) {
if (LAYOUT[r][c] === 1) { if (LAYOUT[r][c] === 1) {
const x = c * this.TW; const x = OX + c * TW;
const y = r * this.TH; const y = r * TH;
g.fillStyle(0x0000aa); g.fillStyle(0x0000cc);
g.fillRect(x + 1, y + 1, this.TW - 2, this.TH - 2); g.fillRect(x + 1, y + 1, TW - 2, TH - 2);
g.lineStyle(2, 0x4444ff); g.lineStyle(2, 0x4455ff);
g.strokeRect(x + 2, y + 2, this.TW - 4, this.TH - 4); g.strokeRect(x + 3, y + 3, TW - 6, TH - 6);
} }
} }
} }
@ -273,19 +329,14 @@ export default class DotDude extends Phaser.Scene {
for (let r = 0; r < ROWS; r++) { for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) { for (let c = 0; c < COLS; c++) {
if (this.dots[r][c] === 1) { if (this.dots[r][c] === 1) {
const x = (c + 0.5) * this.TW;
const y = (r + 0.5) * this.TH;
g.fillStyle(0xffd700); g.fillStyle(0xffd700);
g.fillCircle(x, y, this.cfg.dotRadius); g.fillCircle(this._px(c), this._py(r), this.cfg.dotRadius);
} }
} }
} }
// Ghosts // Ghosts
this.ghosts.forEach(gh => { this.ghosts.forEach(gh => { if (gh.active) this._drawGhost(g, gh.x, gh.y, gh.color); });
if (!gh.active) return;
this._drawGhost(g, gh.x, gh.y, gh.color);
});
// Player // Player
this._drawPlayer(g, this.player.x, this.player.y, this.player.dir, this.mouthAngle); this._drawPlayer(g, this.player.x, this.player.y, this.player.dir, this.mouthAngle);
@ -293,10 +344,8 @@ export default class DotDude extends Phaser.Scene {
_drawPlayer(g, x, y, dir, mouthAngle) { _drawPlayer(g, x, y, dir, mouthAngle) {
const r = 18; const r = 18;
// Yellow circle
g.fillStyle(0xffff00); g.fillStyle(0xffff00);
g.fillCircle(x, y, r); g.fillCircle(x, y, r);
// Black mouth triangle cutout
const rot = Math.atan2(dir.dy, dir.dx); const rot = Math.atan2(dir.dy, dir.dx);
g.fillStyle(0x000022); g.fillStyle(0x000022);
g.fillTriangle( g.fillTriangle(
@ -304,20 +353,16 @@ export default class DotDude extends Phaser.Scene {
x + Math.cos(rot - mouthAngle) * (r + 2), y + Math.sin(rot - mouthAngle) * (r + 2), x + Math.cos(rot - mouthAngle) * (r + 2), y + Math.sin(rot - mouthAngle) * (r + 2),
x + Math.cos(rot + mouthAngle) * (r + 2), y + Math.sin(rot + mouthAngle) * (r + 2) x + Math.cos(rot + mouthAngle) * (r + 2), y + Math.sin(rot + mouthAngle) * (r + 2)
); );
// Eye
g.fillStyle(0x000000); g.fillStyle(0x000000);
g.fillCircle(x + Math.cos(rot - 0.8) * 8, y + Math.sin(rot - 0.8) * 8, 3); g.fillCircle(x + Math.cos(rot - 0.8) * 8, y + Math.sin(rot - 0.8) * 8, 3);
} }
_drawGhost(g, x, y, color) { _drawGhost(g, x, y, color) {
// Body
g.fillStyle(color); g.fillStyle(color);
g.fillCircle(x, y - 6, 16); g.fillCircle(x, y - 6, 16);
g.fillRect(x - 16, y - 6, 32, 20); g.fillRect(x - 16, y - 6, 32, 20);
// Wavy bottom
g.fillTriangle(x - 16, y + 14, x - 8, y + 20, x, y + 14); g.fillTriangle(x - 16, y + 14, x - 8, y + 20, x, y + 14);
g.fillTriangle(x, y + 14, x + 8, y + 20, x + 16, y + 14); g.fillTriangle(x, y + 14, x + 8, y + 20, x + 16, y + 14);
// Eyes
g.fillStyle(0xffffff); g.fillStyle(0xffffff);
g.fillCircle(x - 6, y - 8, 5); g.fillCircle(x - 6, y - 8, 5);
g.fillCircle(x + 6, y - 8, 5); g.fillCircle(x + 6, y - 8, 5);