71 lines
3.9 KiB
JavaScript
71 lines
3.9 KiB
JavaScript
import { safeName } from '../content-policy.js';
|
|
|
|
export const name = 'flappy';
|
|
export const description = 'Play ASCII Flappy and save scores';
|
|
|
|
function grid(width, height) { return Array.from({ length: height }, () => Array(width).fill(' ')); }
|
|
function frame(cells) { return `+${'-'.repeat(cells[0].length)}+\n${cells.map(row => `|${row.join('')}|`).join('\n')}\n+${'-'.repeat(cells[0].length)}+`; }
|
|
|
|
export class FlappyGame {
|
|
constructor(random = Math.random) { this.random = random; this.width = 30; this.height = 12; this.reset(); }
|
|
reset() { this.y = 5; this.velocity = 0; this.score = 0; this.over = false; this.recorded = false; this.pipes = [{ x: 22, gap: this.newGap() }]; }
|
|
newGap() { return 2 + Math.floor(this.random() * (this.height - 6)); }
|
|
tick(flap = false) {
|
|
if (this.over) return;
|
|
this.velocity = flap ? -0.65 : Math.min(0.7, this.velocity + 0.16); this.y += this.velocity;
|
|
for (const pipe of this.pipes) pipe.x--;
|
|
if (this.pipes[0].x < -1) this.pipes.shift();
|
|
const last = this.pipes.at(-1);
|
|
if (last.x < 17) this.pipes.push({ x: this.width - 1, gap: this.newGap() });
|
|
for (const pipe of this.pipes) {
|
|
if (pipe.x === 4) this.score++;
|
|
if ((pipe.x === 4 || pipe.x === 5) && (this.y < pipe.gap || this.y > pipe.gap + 3)) this.over = true;
|
|
}
|
|
if (this.y < 0 || this.y >= this.height) this.over = true;
|
|
}
|
|
render() {
|
|
const cells = grid(this.width, this.height);
|
|
for (const pipe of this.pipes) if (pipe.x >= 0 && pipe.x < this.width) for (let y = 0; y < this.height; y++) if (y < pipe.gap || y > pipe.gap + 3) cells[y][pipe.x] = '#';
|
|
const birdY = Math.round(this.y);
|
|
if (birdY >= 0 && birdY < this.height) cells[birdY][4] = '>';
|
|
return `${frame(cells)}\nScore: ${this.score}${this.over ? ' GAME OVER' : ''}`;
|
|
}
|
|
}
|
|
|
|
export class FlappyService {
|
|
constructor(db) { this.db = db; this.live = new WeakMap(); }
|
|
draw(ctx, text, help) { if (!ctx.channel?.destroyed) ctx.channel.write(`\x1b[?25l\x1b[2J\x1b[H${text}\r\n\r\n${help}`.replace(/\n/g, '\r\n')); }
|
|
display(ctx, game) {
|
|
const player = safeName(ctx.username); const scores = this.db.getLeaderboard('flappy', 3);
|
|
const board = scores.length ? scores.map((row, index) => `${index + 1}. ${row.player.slice(0, 16).padEnd(16)} ${row.score}`).join('\n') : 'No scores yet';
|
|
return `${game.render()}\n\nYour high score: ${Math.max(game.score, this.db.getPlayerBest('flappy', player))}\nTop Flappy\n${board}`;
|
|
}
|
|
stop(ctx, message = '') {
|
|
const state = this.live.get(ctx); if (state?.timer) clearInterval(state.timer);
|
|
this.live.delete(ctx); ctx.tui?.setInputHandler(null);
|
|
if (message) ctx.tui?.finishLive(`\x1b[?25h${message}`);
|
|
}
|
|
start(ctx) {
|
|
if (!ctx.tui) return 'Flappy is interactive; connect with an SSH shell (without a remote command).';
|
|
this.stop(ctx); const game = new FlappyGame(); const state = { game, timer: null, pendingFlap: false }; this.live.set(ctx, state);
|
|
const finish = () => {
|
|
if (!game.recorded) { this.db.addScore('flappy', safeName(ctx.username), game.score); game.recorded = true; }
|
|
if (state.timer) clearInterval(state.timer); state.timer = null;
|
|
this.draw(ctx, this.display(ctx, game), 'R: restart Q: quit');
|
|
};
|
|
const update = flap => { game.tick(flap); if (game.over) finish(); else this.draw(ctx, this.display(ctx, game), 'SPACE: flap Q: quit'); };
|
|
ctx.tui.setInputHandler(input => {
|
|
if (input.includes('\x03') || /q/i.test(input) || input.includes('\x1b')) this.stop(ctx, 'Flappy exited.');
|
|
else if (game.over && /r/i.test(input)) this.start(ctx);
|
|
else if (input.includes(' ')) state.pendingFlap = true;
|
|
return true;
|
|
});
|
|
state.timer = setInterval(() => { update(state.pendingFlap); state.pendingFlap = false; }, 180);
|
|
this.draw(ctx, this.display(ctx, game), 'SPACE: flap Q: quit');
|
|
return '__LIVE__';
|
|
}
|
|
disconnect(ctx) { this.stop(ctx); }
|
|
}
|
|
|
|
export function run({ ctx, api }) { return api.flappy.start(ctx); }
|