Skip to content

Commit

Permalink
✨ handle quits and game-overs, optimize startup
Browse files Browse the repository at this point in the history
  • Loading branch information
ctcpip committed Jun 1, 2024
1 parent de6b5e7 commit 73805b4
Show file tree
Hide file tree
Showing 9 changed files with 1,074 additions and 219 deletions.
2 changes: 0 additions & 2 deletions .github/workflows/yee.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,10 @@ jobs:

steps:
- uses: actions/checkout@v4

- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'

- run: npm ci
- run: npm test
35 changes: 27 additions & 8 deletions board.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const { shapes } = require('./shapes');
const Shape = require('./shape');
const directions = require('./directions');
const Rando = require('./rando');
const { messageTypeEnum } = require('netrisse-lib');

const BOARD_WIDTH = 20;

Expand Down Expand Up @@ -54,6 +55,10 @@ module.exports = class Board {
}
}

get isMainBoard() {
return this.game.boards.length === 0 || this.game.boards[0] === this;
}

topBorder = '┏━━━━━━━━━━━━━━━━━━━━┓';
bottomBorder = '┗━━━━━━━━━━━━━━━━━━━━┛';
heldShapeTopBorder = '┏━━━━━━━━━━┓';
Expand Down Expand Up @@ -121,12 +126,12 @@ module.exports = class Board {
this.screen.d(this.left, this.bottom, this.bottomBorder);
}

drawGameOver() {
const lines = `IT'S CURTAINS FOR YOU!`.split(' ');
const firstLineY = Math.floor(this.bottom / 2) - 4;
drawGameOver(text) {
const lines = text.split(' ');
const firstLineY = Math.floor(this.bottom / 2) - 7;

// clear out some space on the board
for (let i = 0; i < 6; i++) {
for (let i = 0; i < 9; i++) {
for (let i2 = this.left + 1; i2 < this.right; i2++) {
this.screen.d(i2, firstLineY - 1 + i, ' ');
}
Expand All @@ -137,6 +142,12 @@ module.exports = class Board {
const l = lines[i];
this.screen.d(Math.ceil((this.right + this.left) / 2) - Math.ceil((l.length / 2)), firstLineY + i, l, { color: 'brightRed' });
}

const scoreLines = [`Score: ${this.score}`, `Lines: ${this.linesCleared}`];
for (let i = 0; i < scoreLines.length; i++) {
const l = scoreLines[i];
this.screen.d(Math.ceil((this.right + this.left) / 2) - Math.ceil((l.length / 2)), firstLineY + i + lines.length + 1, l);
}
}

drawScore() {
Expand Down Expand Up @@ -279,7 +290,7 @@ module.exports = class Board {

if (gameOver) {
this.gameOver = true;
this.drawGameOver();
this.drawGameOver(`IT'S CURTAINS FOR YOU!`);
this.screen.render();
}
else {
Expand Down Expand Up @@ -327,7 +338,7 @@ module.exports = class Board {
}

if (this.isMainBoard) {
this.game.client?.sendMessage({}, this.game.client.messageTypeEnum.HOLD);
this.game.client?.sendMessage(messageTypeEnum.HOLD);
}

this.stopAutoMoveTimer();
Expand Down Expand Up @@ -495,7 +506,15 @@ module.exports = class Board {
this.screen.render();
}

get isMainBoard() {
return this.game.boards.length === 0 || this.game.boards[0] === this;
quit(isGameOver) {
// they could quit before the game is started
if (this.game.started) {
this.clearLines(true);
}
const txt = isGameOver ? 'GAME OVER' : `PLAYER HAS QUIT 😿`;

this.gameOver = true;
this.drawGameOver(txt);
this.screen.render();
}
};
26 changes: 10 additions & 16 deletions client.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,8 @@
const WS = require('ws');
const uuid = require('uuid');
const { Message, messageTypeEnum } = require('netrisse-lib');

module.exports = class NetrisseClient {
messageTypeEnum = Object.freeze({
CONNECT: 0,
DIRECTION: 1,
HOLD: 2,
JUNK: 3,
PAUSE: 4,
QUIT: 5,
SEED: 6,
UNPAUSE: 7,
});

constructor(gameID, server = 'localhost:4752') {
if (typeof gameID === 'undefined') {
throw new Error('gameID cannot be undefined!');
Expand All @@ -33,16 +23,20 @@ module.exports = class NetrisseClient {
});

this.ws.on('open', () => {
this.sendMessage({ seed }, this.messageTypeEnum.CONNECT);
this.sendMessage(messageTypeEnum.CONNECT, { seed });
});
}

disconnect() {
this.ws.close(4333, JSON.stringify({ type: this.messageTypeEnum.QUIT, playerID: this.playerID, gameID: this.gameID }));
this.ws.close(4333, JSON.stringify({ type: messageTypeEnum.QUIT, playerID: this.playerID, gameID: this.gameID }));
}

sendMessage(o = {}, type) {
this.ws.send(JSON.stringify(Object.assign(o, { type, playerID: this.playerID, gameID: this.gameID })));
// check error here
sendMessage(type, o) {
this.ws.send(new Message(type, this.playerID, this.gameID, o).serialize(),
err => {
if (err) {
throw new Error(err);
}
});
}
};
18 changes: 12 additions & 6 deletions game.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const { messageTypeEnum } = require('netrisse-lib');

module.exports = class Game {
pauses = []; // array of player ids
boards = [];
Expand All @@ -10,7 +12,7 @@ module.exports = class Game {
this.currentPlayerID = currentPlayerID;
}

pause(isPausing, playerID, isRemote) {
pause(isPausing, playerID) {
const previousPauses = this.pauses.length;

if (isPausing) {
Expand All @@ -37,16 +39,16 @@ module.exports = class Game {
this.start();
}

this.togglePauseBoard(isRemote);
this.togglePauseBoard();
}
else if (previousPauses === 0 && this.pauses.length > 0) {
this.togglePauseBoard(isRemote);
this.togglePauseBoard();
}
}

togglePauseBoard(isRemote) {
togglePauseBoard() {
// no need to call pause() on the other player boards
this.boards[0].pause(isRemote);
this.boards[0].pause();
}

get isPaused() {
Expand Down Expand Up @@ -94,7 +96,11 @@ module.exports = class Game {
}
}

this.client.sendMessage({ junkLines, toPlayerID: toBoard.playerID }, this.client.messageTypeEnum.JUNK);
this.client.sendMessage(messageTypeEnum.JUNK, { junkLines, toPlayerID: toBoard.playerID });
toBoard.receiveJunk(junkLines);
}

gameOver() {
this.boards.forEach(b => b.quit(true));
}
};
100 changes: 35 additions & 65 deletions netrisse.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ const directions = require('./directions');
const algorithms = require('./algorithms');
const MersenneTwister = require('mersenne-twister');
const NetrisseClient = require('./client');
const { debug, messageTypeEnum } = require('netrisse-lib');
const withResolvers = require('promise.withresolvers');

withResolvers.shim();

(async () => {
// multiplayer game modes: battle (default), friendly
Expand All @@ -22,7 +26,7 @@ const NetrisseClient = require('./client');

const mainBoardPosition = [2, 21, 23, 0]; // top, right, bottom, left

let seed = new MersenneTwister().random_int();
const seed = new MersenneTwister().random_int();
// const seed = 3103172451;

let thisPlayerIsPaused = false;
Expand All @@ -31,96 +35,87 @@ const NetrisseClient = require('./client');
let players = 1;
players += 1;

let client, game, screen; // eslint-disable-line prefer-const
let client, game, screen, promiseSeed, resolveSeed, rejectSeed, seedFromServer; // eslint-disable-line prefer-const

if (players > 1) {
client = new NetrisseClient('snoofism');
client.connect(seed);

thisPlayerID = client.playerID;

let seedFromServer;
// todo: add timeout to call rejectSeed
({ promise: promiseSeed, resolve: resolveSeed, reject: rejectSeed } = Promise.withResolvers()); // eslint-disable-line no-unused-vars

client.ws.on('message', async rawData => {
// QUIT: 4, client
// QUIT: 4, server

debug(`${thisPlayerID} got ${rawData}`);
const message = JSON.parse(rawData);

// need to change to use the correct board for the player who sent the message
switch (message.type) {
case client.messageTypeEnum.CONNECT:
case messageTypeEnum.CONNECT:
{
await retry(0.25, 100, () => !game); // nasty, fix

if (!game) {
throw new Error('sadness :(');
}
seedFromServer = await promiseSeed;

for (const p of message.players) {
if (p !== thisPlayerID) {
const xOffset = 1;
const boardPosition = [mainBoardPosition[0], (mainBoardPosition[1] * 3) + xOffset, mainBoardPosition[2], (mainBoardPosition[1] * 2) + xOffset];
const b = new Board(...boardPosition, screen, game, seed);
const b = new Board(...boardPosition, screen, game, seedFromServer);
game.boards.push(b);
game.pause(true, p, true);
game.pause(true, p);
}
}

break;
}

case client.messageTypeEnum.DIRECTION:
case messageTypeEnum.DIRECTION:
game.boards[1].currentShape.move(message.direction);
break;
case client.messageTypeEnum.HOLD:
case messageTypeEnum.GAME_OVER:
game.gameOver();
quit();
break;
case messageTypeEnum.HOLD:
game.boards[1].holdShape();
break;

case client.messageTypeEnum.JUNK:
case messageTypeEnum.JUNK:
{
const b = game.boards.find(b2 => b2.playerID === message.toPlayerID);
b.receiveJunk(message.junkLines);
break;
}

case client.messageTypeEnum.PAUSE:
game.pause(true, message.playerID, true);
case messageTypeEnum.PAUSE:
game.pause(true, message.playerID);
break;
case client.messageTypeEnum.QUIT:
// do something
case messageTypeEnum.QUIT:
game.boards[1].quit();
break;
case client.messageTypeEnum.SEED:
seedFromServer = message.seed;
case messageTypeEnum.SEED:
resolveSeed(message.seed);
break;
case client.messageTypeEnum.UNPAUSE:
game.pause(false, message.playerID, true);
case messageTypeEnum.UNPAUSE:
game.pause(false, message.playerID);
break;
default:
throw new Error(`unsupported message type: ${message.type}`);
}
});

await retry(0.25, 100, () => !seedFromServer); // nasty, fix

if (seedFromServer) {
seed = seedFromServer;
}
else {
throw new Error('unable to get seed from server :(');
}
}

screen = new Screen(colorEnabled, interval, seed);
seedFromServer = await promiseSeed;
screen = new Screen(colorEnabled, interval, seedFromServer);
game = new Game(interval, algorithms.frustrationFree, client, thisPlayerID);
const board = new Board(...mainBoardPosition, screen, game, seed);
const board = new Board(...mainBoardPosition, screen, game, seedFromServer);

game.boards.push(board);

if (players > 1) {
// for a multi-player game, pause at the start to allow players to join
thisPlayerIsPaused = true;
game.pause(true, thisPlayerID, false);
game.pause(true, thisPlayerID);
}

function quit() {
Expand Down Expand Up @@ -181,10 +176,10 @@ const NetrisseClient = require('./client');
{
thisPlayerIsPaused = !thisPlayerIsPaused;

const messageType = thisPlayerIsPaused ? client.messageTypeEnum.PAUSE : client.messageTypeEnum.UNPAUSE;
const messageType = thisPlayerIsPaused ? messageTypeEnum.PAUSE : messageTypeEnum.UNPAUSE;

client?.sendMessage({}, messageType);
game.pause(thisPlayerIsPaused, thisPlayerID, false);
client?.sendMessage(messageType, {});
game.pause(thisPlayerIsPaused, thisPlayerID);
break;
}

Expand All @@ -198,29 +193,4 @@ const NetrisseClient = require('./client');
break;
}
});

/**
* Retry a function until it returns false, or timeout expires
* @param {number} timeout fractional minutes - length of time to retry
* @param {number} pauseRate milliseconds - time to pause between retries (frequency)
* @param {function} retryFunction function to retry - must return a boolean - function will be retried until it returns false
*/
async function retry(timeout, pauseRate, retryFunction) {
const start = new Date();

while (new Date() - start < timeout * 60000 && await retryFunction()) {
await pause(pauseRate);
}
}

/**
* Pause thread for an amount of time
* @param {number} ms milliseconds to pause
* @returns {promise}
* @example
* await pause(1 * 1000); // pause for 1 second
*/
function pause(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
})();
Loading

0 comments on commit 73805b4

Please sign in to comment.