Improve Pong mode selection and ball physics
This commit is contained in:
+37
-5
@@ -19,9 +19,22 @@ export class PongGame {
|
|||||||
this.move(side, action);
|
this.move(side, action);
|
||||||
if (this.bot && this.random() > 0.25) this.rightY = Math.max(0, Math.min(this.height - 3, this.rightY + Math.sign(this.ballY - (this.rightY + 1))));
|
if (this.bot && this.random() > 0.25) this.rightY = Math.max(0, Math.min(this.height - 3, this.rightY + Math.sign(this.ballY - (this.rightY + 1))));
|
||||||
let nextX = this.ballX + this.vx; let nextY = this.ballY + this.vy;
|
let nextX = this.ballX + this.vx; let nextY = this.ballY + this.vy;
|
||||||
if (nextY < 0 || nextY >= this.height) { this.vy *= -1; nextY = this.ballY + this.vy; }
|
if (nextY < 0 || nextY >= this.height) {
|
||||||
if (this.vx < 0 && nextX <= 1 && nextY >= this.leftY && nextY <= this.leftY + 2) { this.vx = 1; nextX = 2; }
|
this.vy *= -1;
|
||||||
if (this.vx > 0 && nextX >= this.width - 2 && nextY >= this.rightY && nextY <= this.rightY + 2) { this.vx = -1; nextX = this.width - 3; }
|
nextY = Math.max(0, Math.min(this.height - 1, this.ballY + this.vy));
|
||||||
|
}
|
||||||
|
const bounce = (paddleY, direction, x) => {
|
||||||
|
const hit = nextY - (paddleY + 1);
|
||||||
|
if (Math.abs(hit) > 1) return false;
|
||||||
|
// Flat center returns are harder to exploit, so accelerate them slightly.
|
||||||
|
this.vx = direction * (Math.abs(hit) < 0.1 ? 1.25 : 1);
|
||||||
|
// The contact point controls the angle: corners produce the steepest deflection.
|
||||||
|
this.vy = hit * 1.2;
|
||||||
|
nextX = x;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
if (this.vx < 0 && nextX <= 1) bounce(this.leftY, 1, 2);
|
||||||
|
if (this.vx > 0 && nextX >= this.width - 2) bounce(this.rightY, -1, this.width - 3);
|
||||||
this.ballX = nextX; this.ballY = nextY;
|
this.ballX = nextX; this.ballY = nextY;
|
||||||
if (this.ballX < 0) { this.rightScore++; this.serve(1); }
|
if (this.ballX < 0) { this.rightScore++; this.serve(1); }
|
||||||
if (this.ballX >= this.width) { this.leftScore++; this.serve(-1); }
|
if (this.ballX >= this.width) { this.leftScore++; this.serve(-1); }
|
||||||
@@ -31,7 +44,8 @@ export class PongGame {
|
|||||||
render(side = 'left') {
|
render(side = 'left') {
|
||||||
const cells = grid(this.width, this.height);
|
const cells = grid(this.width, this.height);
|
||||||
for (let i = 0; i < 3; i++) { cells[this.leftY + i][1] = '|'; cells[this.rightY + i][this.width - 2] = '|'; }
|
for (let i = 0; i < 3; i++) { cells[this.leftY + i][1] = '|'; cells[this.rightY + i][this.width - 2] = '|'; }
|
||||||
if (this.ballY >= 0 && this.ballY < this.height && this.ballX >= 0 && this.ballX < this.width) cells[this.ballY][this.ballX] = 'o';
|
const renderX = Math.round(this.ballX); const renderY = Math.round(this.ballY);
|
||||||
|
if (renderY >= 0 && renderY < this.height && renderX >= 0 && renderX < this.width) cells[renderY][renderX] = 'o';
|
||||||
const label = side === 'left' ? `${this.leftScore} - ${this.rightScore}` : `${this.rightScore} - ${this.leftScore}`;
|
const label = side === 'left' ? `${this.leftScore} - ${this.rightScore}` : `${this.rightScore} - ${this.leftScore}`;
|
||||||
return `${frame(cells)}\nYou - Opponent: ${label}${this.over ? ' MATCH OVER' : ''}`;
|
return `${frame(cells)}\nYou - Opponent: ${label}${this.over ? ' MATCH OVER' : ''}`;
|
||||||
}
|
}
|
||||||
@@ -45,6 +59,24 @@ export class PongService {
|
|||||||
this.live.delete(ctx); ctx.tui?.setInputHandler(null);
|
this.live.delete(ctx); ctx.tui?.setInputHandler(null);
|
||||||
if (message) ctx.tui?.finishLive(`\x1b[?25h${message}`);
|
if (message) ctx.tui?.finishLive(`\x1b[?25h${message}`);
|
||||||
}
|
}
|
||||||
|
chooseMode(ctx) {
|
||||||
|
if (!ctx.tui) return 'Pong is interactive; connect with an SSH shell (without a remote command).';
|
||||||
|
let selected = 0;
|
||||||
|
const modes = [['BOT', 'Play against the computer'], ['ONLINE', 'Play against another connected visitor']];
|
||||||
|
const render = () => this.draw(ctx, `PONG\n\n${modes.map(([name, description], index) => `${index === selected ? '>' : ' '} ${name.padEnd(7)} ${description}`).join('\n')}`, 'UP/DOWN arrows: choose ENTER: select Q: cancel');
|
||||||
|
ctx.tui.setInputHandler(input => {
|
||||||
|
if (input.includes('\x03') || /q/i.test(input)) { this.stop(ctx, '\x1b[?25hPong cancelled.'); return true; }
|
||||||
|
if (input.includes('\x1b[A') || input.includes('\x1bOA')) selected = (selected + modes.length - 1) % modes.length;
|
||||||
|
if (input.includes('\x1b[B') || input.includes('\x1bOB')) selected = (selected + 1) % modes.length;
|
||||||
|
if (input.includes('\r') || input.includes('\n')) {
|
||||||
|
const result = this.start(ctx, modes[selected][0].toLowerCase());
|
||||||
|
if (result !== '__LIVE__') ctx.tui?.finishLive(`\x1b[?25h${result}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
render(); return true;
|
||||||
|
});
|
||||||
|
render(); return '__LIVE__';
|
||||||
|
}
|
||||||
start(ctx, mode) {
|
start(ctx, mode) {
|
||||||
if (!['bot', 'online'].includes(mode)) return 'Usage: pong [bot|online]';
|
if (!['bot', 'online'].includes(mode)) return 'Usage: pong [bot|online]';
|
||||||
if (!ctx.tui) return 'Pong is interactive; connect with an SSH shell (without a remote command).';
|
if (!ctx.tui) return 'Pong is interactive; connect with an SSH shell (without a remote command).';
|
||||||
@@ -84,4 +116,4 @@ export class PongService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function run({ arg, ctx, api }) { return api.pong.start(ctx, arg.toLowerCase() || 'bot'); }
|
export function run({ arg, ctx, api }) { return arg.trim() ? api.pong.start(ctx, arg.toLowerCase()) : api.pong.chooseMode(ctx); }
|
||||||
|
|||||||
@@ -26,6 +26,21 @@ test('pong bot can miss when its reaction is delayed', () => {
|
|||||||
assert.equal(game.rightY, 6);
|
assert.equal(game.rightY, 6);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('pong paddle contact changes trajectory by hit position', () => {
|
||||||
|
const game = new PongGame(false);
|
||||||
|
game.leftY = 4; game.ballX = 2; game.ballY = 4; game.vx = -1; game.vy = 0;
|
||||||
|
game.tick('left', 'stay');
|
||||||
|
assert.equal(game.vx, 1);
|
||||||
|
assert.ok(game.vy < 0, 'upper-edge contact should send the ball upward');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pong flat returns accelerate slightly', () => {
|
||||||
|
const game = new PongGame(false);
|
||||||
|
game.leftY = 4; game.ballX = 2; game.ballY = 5; game.vx = -1; game.vy = 0;
|
||||||
|
game.tick('left', 'stay');
|
||||||
|
assert.equal(game.vx, 1.25);
|
||||||
|
});
|
||||||
|
|
||||||
test('live flappy consumes Space and exits on q', () => {
|
test('live flappy consumes Space and exits on q', () => {
|
||||||
let handler; let finished = ''; let writes = 0;
|
let handler; let finished = ''; let writes = 0;
|
||||||
const service = new FlappyService({ addScore() {}, getLeaderboard() { return []; }, getPlayerBest() { return 0; } });
|
const service = new FlappyService({ addScore() {}, getLeaderboard() { return []; }, getPlayerBest() { return 0; } });
|
||||||
@@ -66,3 +81,15 @@ test('live pong consumes arrow keys and exits on q', () => {
|
|||||||
handler('\x1b[A'); handler('\x1b[B'); handler('q');
|
handler('\x1b[A'); handler('\x1b[B'); handler('q');
|
||||||
assert.match(finished, /left/);
|
assert.match(finished, /left/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('pong mode picker selects online with arrow keys and enter', () => {
|
||||||
|
let handler; let finished = ''; let screen = '';
|
||||||
|
const service = new PongService({ addScore() {}, getLeaderboard() { return []; }, getPlayerBest() { return 0; } });
|
||||||
|
const ctx = { username: 'visitor', channel: { destroyed: false, write(value) { screen = value; } }, tui: {
|
||||||
|
setInputHandler(value) { handler = value; }, finishLive(value) { finished = value; }
|
||||||
|
} };
|
||||||
|
assert.equal(service.chooseMode(ctx), '__LIVE__');
|
||||||
|
assert.match(screen, /BOT/); assert.match(screen, /ONLINE/);
|
||||||
|
handler('\x1b[B'); handler('\r');
|
||||||
|
assert.match(finished, /Waiting for another player/);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user