From 3577a617bc88fd823b9d6c6be50810033aa9464f Mon Sep 17 00:00:00 2001 From: ctcpip Date: Mon, 3 Jun 2024 01:34:58 -0500 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20support=20custom=20online=20game=20?= =?UTF-8?q?names,=20add=20menu=20system,=20config=20persistence?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + README.md | 2 +- eslint.config.js | 1 + package-lock.json | 53 ++++++------ package.json | 28 ++++--- src/board.js | 4 +- src/client.js | 9 +- src/config.js | 76 +++++++++++++++++ src/game.js | 4 +- src/intro.js | 36 ++++++++ src/multiplayer.js | 90 ++++++++++++++++++++ src/netrisse.js | 203 +++------------------------------------------ src/quit.js | 27 ++++++ src/screen.js | 37 ++++----- src/start.js | 145 ++++++++++++++++++++++++++++++++ 15 files changed, 458 insertions(+), 259 deletions(-) create mode 100644 src/config.js create mode 100644 src/intro.js create mode 100644 src/multiplayer.js create mode 100644 src/quit.js create mode 100644 src/start.js diff --git a/.gitignore b/.gitignore index 1669a13..6436308 100644 --- a/.gitignore +++ b/.gitignore @@ -104,3 +104,5 @@ dist .tern-port .DS_Store + +netrisse.config diff --git a/README.md b/README.md index 41d5137..8b89eed 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ a network version of tetris for the console/terminal ## roadmap * [x] single player mode -* [ ] two-player networked mode +* [x] two-player networked mode * [ ] three/four player networked mode ## inspiration diff --git a/eslint.config.js b/eslint.config.js index d224e00..8efd3ee 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2,4 +2,5 @@ const ultraMegaConfig = require('eslint-config-ultra-mega'); module.exports = [ ...ultraMegaConfig, + { languageOptions: { globals: { screen: 'off' } } }, ]; diff --git a/package-lock.json b/package-lock.json index 62da6d7..2bfa2dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,10 +17,10 @@ "ws": "^8.9.0" }, "bin": { - "netrisse": "netrisse.js" + "netrisse": "src/netrisse.js" }, "devDependencies": { - "eslint": "^9.3.0", + "eslint": "^9.4.0", "eslint-config-ultra-mega": "^1.2.0", "madge": "^7.0.0" }, @@ -107,6 +107,19 @@ "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/config-array": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.15.1.tgz", + "integrity": "sha512-K4gzNq+yymn/EVsXYmf+SBcBro8MTf+aXJZUphM96CdzUEr+ClGDvAbpmaEK+cGVigVXIgs9gNmvHAlrzzY5JQ==", + "dependencies": { + "@eslint/object-schema": "^2.1.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", @@ -130,24 +143,19 @@ } }, "node_modules/@eslint/js": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.3.0.tgz", - "integrity": "sha512-niBqk8iwv96+yuTwjM6bWg8ovzAPF9qkICsGtcoa5/dmqcEMfdwNAX7+/OHcJHc7wj7XqPxH98oAHytFYlw6Sw==", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.4.0.tgz", + "integrity": "sha512-fdI7VJjP3Rvc70lC4xkFXHB0fiPeojiL1PxVG6t1ZvXQrarj893PweuBTujxDUFk0Fxj4R7PIIAZ/aiiyZPZcg==", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, + "node_modules/@eslint/object-schema": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.3.tgz", + "integrity": "sha512-HAbhAYKfsAC2EkTqve00ibWIZlaU74Z1EHwAjYr4PXF0YU2VEA1zSIKSSpKszRLRWwHzzRZXvK632u+uXzvsvw==", "engines": { - "node": ">=10.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@humanwhocodes/module-importer": { @@ -162,11 +170,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==" - }, "node_modules/@humanwhocodes/retry": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", @@ -1106,15 +1109,15 @@ } }, "node_modules/eslint": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.3.0.tgz", - "integrity": "sha512-5Iv4CsZW030lpUqHBapdPo3MJetAPtejVW8B84GIcIIv8+ohFaddXsrn1Gn8uD9ijDb+kcYKFUVmC8qG8B2ORQ==", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.4.0.tgz", + "integrity": "sha512-sjc7Y8cUD1IlwYcTS9qPSvGjAC8Ne9LctpxKKu3x/1IC9bnOg98Zy6GxEJUfr1NojMgVPlyANXYns8oE2c1TAA==", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", + "@eslint/config-array": "^0.15.1", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.3.0", - "@humanwhocodes/config-array": "^0.13.0", + "@eslint/js": "9.4.0", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", diff --git a/package.json b/package.json index 75eeba5..272062c 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,18 @@ "name": "netrisse", "description": "a network version of tetris for the console/terminal", "version": "2.0.0", + "author": "Chris de Almeida", "main": "./src/netrisse.js", "bin": { "netrisse": "./src/netrisse.js" }, + "scripts": { + "circles": "madge --extensions js --circular .", + "lint": "eslint .", + "lint:fix": "eslint --fix .", + "netrisse": "node ./src/netrisse.js", + "test": "npm run lint && npm run circles && node ./test/algorithm-test.js" + }, "dependencies": { "mersenne-twister": "^1.1.0", "netrisse-lib": "^2.0.0", @@ -14,35 +22,29 @@ "uuid": "^9.0.0", "ws": "^8.9.0" }, - "scripts": { - "circles": "madge --extensions js --circular .", - "lint": "eslint .", - "lint:fix": "eslint --fix .", - "test": "npm run lint && npm run circles && node ./test/algorithm-test.js" + "devDependencies": { + "eslint": "^9.4.0", + "eslint-config-ultra-mega": "^1.2.0", + "madge": "^7.0.0" }, "repository": { "type": "git", "url": "git+https://github.com/ctcpip/netrisse.git" }, - "author": "Chris de Almeida", - "license": "GPL-3.0-or-later", "bugs": { "url": "https://github.com/ctcpip/netrisse/issues" }, "homepage": "https://github.com/ctcpip/netrisse#readme", - "devDependencies": { - "eslint": "^9.3.0", - "eslint-config-ultra-mega": "^1.2.0", - "madge": "^7.0.0" - }, "pkg": { "assets": "node_modules/terminal-kit/**/*" }, + "license": "GPL-3.0-or-later", "engines": { "node": ">=18" }, "keywords": [ "netris", - "netrisse" + "netrisse", + "tetris" ] } diff --git a/src/board.js b/src/board.js index 312d217..6aed29a 100644 --- a/src/board.js +++ b/src/board.js @@ -21,7 +21,7 @@ module.exports = class Board { linesCleared = 0; gameOver = false; - constructor(top, right, bottom, left, screen, game, seed, playerID) { + constructor({ top, right, bottom, left }, screen, game, seed, playerID) { this.top = top; this.right = right; this.bottom = bottom; @@ -451,7 +451,7 @@ module.exports = class Board { resetAutoMoveTimer() { if (!this.replay && this.isMainBoard) { this.stopAutoMoveTimer(); - this.currentTimeout = setTimeout(this.moveShapeAutomatically.bind(this), this.game.interval); + this.currentTimeout = setTimeout(this.moveShapeAutomatically.bind(this), this.game.speed); } } diff --git a/src/client.js b/src/client.js index 83c4dee..887b2ba 100644 --- a/src/client.js +++ b/src/client.js @@ -18,13 +18,16 @@ module.exports = class NetrisseClient { connect(seed) { this.ws = new WS(`ws://${this.server}`); - this.ws.on('error', err => { - throw new Error(err); // do something here - }); + const { promise, resolve, reject } = Promise.withResolvers(); + + this.ws.on('error', reject); this.ws.on('open', () => { this.sendMessage(messageTypeEnum.CONNECT, { seed }); + resolve(); }); + + return promise; } disconnect() { diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..20fc2d8 --- /dev/null +++ b/src/config.js @@ -0,0 +1,76 @@ +const fs = require('node:fs'); + +class Config { + // can't use private properties with proxies đŸ˜ŋ + _customServer; + _gameName; + _playerName = 'The Unknown Netrisser'; + _speed = 0.5 * 1000; + + get customServer() { + return this._customServer; + } + + set customServer(customServer) { + this._customServer = customServer; + } + + get gameName() { + return this._gameName; + } + + set gameName(gameName) { + if (!gameName) { + throw new Error('game name is required'); + } + this._gameName = gameName; + } + + get playerName() { + return this._playerName; + } + + set playerName(playerName) { + if (playerName) { + this._playerName = playerName; + } + } + + get speed() { + return this._speed; + } + + set speed(speed) { + this._speed = speed; + } + + save() { + try { + fs.writeFileSync(configFile, JSON.stringify(this)); + } + catch (error) { // eslint-disable-line no-unused-vars + // debug(error) + } + } +}; + +const configFile = 'netrisse.config'; +const config = new Config(); + +try { + const savedConfig = JSON.parse(fs.readFileSync(configFile)); + Object.assign(config, savedConfig); +} +catch (error) { // eslint-disable-line no-unused-vars + // debug(error) +} + +const handler = { + set(target, property, value) { + target[property] = value; + target.save(); + return true; + }, +}; + +module.exports = new Proxy(config, handler); diff --git a/src/game.js b/src/game.js index 6d73adb..79dab16 100644 --- a/src/game.js +++ b/src/game.js @@ -5,8 +5,8 @@ module.exports = class Game { boards = []; started = false; - constructor(interval, algorithm, client, currentPlayerID) { - this.interval = interval; + constructor(speed, algorithm, client, currentPlayerID) { + this.speed = speed; this.algorithm = algorithm; this.client = client; this.currentPlayerID = currentPlayerID; diff --git a/src/intro.js b/src/intro.js new file mode 100644 index 0000000..bc61e06 --- /dev/null +++ b/src/intro.js @@ -0,0 +1,36 @@ +const config = require('./config'); +const start = require('./start'); +const quit = require('./quit'); +const multiplayer = require('./multiplayer'); + +module.exports = function intro(screen, seed) { + const mainBoardPosition = { top: 2, right: 21, bottom: 23, left: 0 }; + + let cursorY = 3; + + screen.clear(); + screen.d(2, cursorY, '✨ Welcome to Netrisse! 🎉', { color: 'green' }); + screen.d(5, cursorY += 3, '1) Single player 🧍', { color: 'amber' }); + screen.d(5, cursorY += 2, '2) Multiplayer 🧑‍🤝‍🧑', { color: 'amber' }); + screen.d(5, cursorY += 2, '3) Options 🔧', { color: 'amber' }); + screen.d(5, cursorY += 2, 'Q) Quit đŸšĒ', { color: 'brightred' }); + screen.render(); + + const keyHandler = name => { + switch (name) { + case '1': + screen.term.removeListener('key', keyHandler); + start(seed, screen, mainBoardPosition, config.speed, null); + break; + case '2': + screen.term.removeListener('key', keyHandler); + multiplayer(screen, seed, intro, mainBoardPosition); + break; + case 'CTRL_C': case 'ESCAPE': case 'Q': case 'q': + quit(screen, mainBoardPosition); + break; + } + }; + + screen.term.on('key', keyHandler); +}; diff --git a/src/multiplayer.js b/src/multiplayer.js new file mode 100644 index 0000000..c5b18ec --- /dev/null +++ b/src/multiplayer.js @@ -0,0 +1,90 @@ +const NetrisseClient = require('./client'); +const config = require('./config'); +const quit = require('./quit'); +const start = require('./start'); + +module.exports = function(screen, seed, intro, mainBoardPosition) { + screen.clear(); + let cursorY = 3; + + screen.d(2, cursorY, '✨ Welcome to Netrisse! 🎉', { color: 'green' }); + screen.d(5, cursorY += 3, '1) Create / join game 🕹ī¸', { color: 'amber' }); + screen.d(5, cursorY += 2, '2) Back 🔙', { color: 'amber' }); + screen.d(5, cursorY += 2, 'Q) Quit đŸšĒ', { color: 'brightred' }); + + screen.render(); + + const keyHandler = name => { + switch (name) { + case '1': + { + screen.clear(); + let cursorX = 5; + let cursorY = 3; + + screen.d(2, cursorY, '✨ Welcome to Netrisse! 🎉', { color: 'green' }); + screen.d(5, cursorY += 3, '1) Create / join game 🕹ī¸', { color: 'amber' }); + + const playerNamePrompt = 'Enter your name: '; + screen.d(cursorX += 4, cursorY += 2, playerNamePrompt, { color: 'brightmagenta' }); + screen.render(); + + const playerNameInputCursor = { x: cursorX + playerNamePrompt.length + 1, y: cursorY += 1 }; + let gameNameInputCursor; + + screen.term + .inputField( + { ...playerNameInputCursor, default: config.playerName }, + ).promise + .then(playerName => { + config.playerName = playerName; + screen.d(playerNameInputCursor.x - 1, playerNameInputCursor.y - 1, playerName); + + const gameNamePrompt = 'Enter game name: '; + screen.d(cursorX, cursorY += 1, gameNamePrompt, { color: 'brightmagenta' }); + screen.render(); + + gameNameInputCursor = { x: cursorX + gameNamePrompt.length + 1, y: cursorY += 1 }; + + return screen.term + .inputField( + { ...gameNameInputCursor, default: config.gameName }, + ) + .promise; + }) + .then(gameName => { + screen.term.hideCursor(); + config.gameName = gameName; + screen.d(gameNameInputCursor.x - 1, gameNameInputCursor.y - 1, gameName); + screen.render(); + + const client = new NetrisseClient(config.gameName); + client.connect(seed) + .then(() => { + start(null, screen, mainBoardPosition, config.speed, client); + }) + .catch(err => { + const errorY = gameNameInputCursor.y + 1; + screen.d(cursorX + 2, errorY, 'Failed to connect:', { color: 'brightred' }); + screen.d(cursorX + 4, errorY + 1, err.errors.map(e => e.message), { color: 'brightred' }); + screen.d(5, errorY + 3, '2) Back 🔙', { color: 'amber' }); + screen.render(); + }); + }); + + screen.term.hideCursor(false); + + break; + } + case '2': + screen.term.removeListener('key', keyHandler); + intro(screen, seed); + break; + case 'CTRL_C': case 'ESCAPE': case 'Q': case 'q': + quit(screen, mainBoardPosition); + break; + } + }; + + screen.term.on('key', keyHandler); +}; diff --git a/src/netrisse.js b/src/netrisse.js index f0a2eb9..3dcf91b 100644 --- a/src/netrisse.js +++ b/src/netrisse.js @@ -1,196 +1,19 @@ -const Board = require('./board'); -const Screen = require('./screen'); -const Game = require('./game'); -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 - // need to wait to start the game until all players are ready (2nd board is not null) - - // need to deal with concurrency issues -- what if p1 paused the game, p2 does a hold (or move), successful on p2 screen but not p1 screen because game paused - // probably change the logic to always allow the movement if it was not the main board - - // netris actually has a separate pause per player... - // so basically, there can be _n_ pauses and only if it's 0 does the game continue - - const colorEnabled = true; - const interval = 0.5 * 1000; - // const interval = 30 * 1000; - - const mainBoardPosition = [2, 21, 23, 0]; // top, right, bottom, left - - const seed = new MersenneTwister().random_int(); - // const seed = 3103172451; - - let thisPlayerIsPaused = false; - let thisPlayerID = 0; - - let players = 1; - players += 1; - - 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; - - // todo: add timeout to call rejectSeed - ({ promise: promiseSeed, resolve: resolveSeed, reject: rejectSeed } = Promise.withResolvers()); // eslint-disable-line no-unused-vars +#!/usr/bin/env node - client.ws.on('message', async rawData => { - 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 messageTypeEnum.CONNECT: - { - 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, seedFromServer); - game.boards.push(b); - game.pause(true, p); - } - } - - break; - } - - case messageTypeEnum.DIRECTION: - game.boards[1].currentShape.move(message.direction); - break; - case messageTypeEnum.GAME_OVER: - game.gameOver(); - quit(); - break; - case messageTypeEnum.HOLD: - game.boards[1].holdShape(); - break; - - case messageTypeEnum.JUNK: - { - const b = game.boards.find(b2 => b2.playerID === message.toPlayerID); - b.receiveJunk(message.junkLines); - break; - } - - case messageTypeEnum.PAUSE: - game.pause(true, message.playerID); - break; - case messageTypeEnum.QUIT: - game.boards[1].quit(); - break; - case messageTypeEnum.SEED: - resolveSeed(message.seed); - break; - case messageTypeEnum.UNPAUSE: - game.pause(false, message.playerID); - break; - default: - throw new Error(`unsupported message type: ${message.type}`); - } - }); - } - - seedFromServer = await promiseSeed; - screen = new Screen(colorEnabled, interval, seedFromServer); - game = new Game(interval, algorithms.frustrationFree, client, thisPlayerID); - 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); - } - - function quit() { - for (const b of game.boards) { - b.stopAutoMoveTimer(); - } - - clearTimeout(screen.timeDisplayTimeout); - screen.term.grabInput(false); - screen.term.moveTo(board.left + 1, board.bottom + 1); - screen.term.eraseLine(); - screen.term.hideCursor(false); - // writeDebugInfo(); - - if (client) { - client.disconnect(); - } - } - - function writeDebugInfo() { // eslint-disable-line no-unused-vars - console.log(`seed: ${seed}`); - console.log(JSON.stringify(board.moves)); - } +const MersenneTwister = require('mersenne-twister'); +const Screen = require('./screen'); +const intro = require('./intro'); - screen.term.on('key', name => { - // the called methods should send the necessary message to the server, as there is no point in sending if it's a no-op (quick return) - switch (name) { - case 'j': - case 'J': - case 'LEFT': - board.currentShape?.move(directions.LEFT); - break; - case 'k': - case 'K': - case 'UP': - board.currentShape?.move(directions.ROTATE_LEFT); - break; - case 'l': - case 'L': - case 'RIGHT': - board.currentShape?.move(directions.RIGHT); - break; - case ' ': - board.currentShape?.move(directions.DROP); - break; - case 'm': - case 'M': - case 'DOWN': - board.currentShape?.move(directions.DOWN); - break; - case 'h': - case 'H': - board.holdShape(); - break; +// multiplayer game modes: battle (default), friendly +// need to wait to start the game until all players are ready (2nd board is not null) - case 'p': - case 'P': - { - thisPlayerIsPaused = !thisPlayerIsPaused; +// need to deal with concurrency issues -- what if p1 paused the game, p2 does a hold (or move), successful on p2 screen but not p1 screen because game paused +// probably change the logic to always allow the movement if it was not the main board - const messageType = thisPlayerIsPaused ? messageTypeEnum.PAUSE : messageTypeEnum.UNPAUSE; +const colorEnabled = true; +const screen = new Screen(colorEnabled); - client?.sendMessage(messageType, {}); - game.pause(thisPlayerIsPaused, thisPlayerID); - break; - } +const seed = new MersenneTwister().random_int(); +// const seed = 3103172451; - case 'CTRL_C': - case 'Q': - case 'q': - case 'ESCAPE': - quit(); - break; - default: - break; - } - }); -})(); +intro(screen, seed); diff --git a/src/quit.js b/src/quit.js new file mode 100644 index 0000000..5aab9ed --- /dev/null +++ b/src/quit.js @@ -0,0 +1,27 @@ +module.exports = function(screen, mainBoardPosition, game, client) { + if (game) { + for (const b of game.boards) { + b.stopAutoMoveTimer(); + } + game.boards[0].quit(); + } + + clearTimeout(screen.timeDisplayTimeout); + + screen.term.moveTo(mainBoardPosition.left + 1, mainBoardPosition.bottom + 1); + screen.term.eraseLine(); + screen.term.hideCursor(false); + // writeDebugInfo(); + + if (client) { + // add reject timeout + client.ws.on('close', () => { // ensure we send disconnect to the server before we exit + screen.term.grabInput(false); // stop listening for input so the process exits + }); + + client.disconnect(); + } + else { + screen.term.grabInput(false); // stop listening for input so the process exits + } +}; diff --git a/src/screen.js b/src/screen.js index 89de0be..661fbcc 100644 --- a/src/screen.js +++ b/src/screen.js @@ -2,29 +2,18 @@ const termkit = require('terminal-kit'); const packageJSON = require('../package.json'); module.exports = class Screen { - constructor(colorEnabled, interval, seed) { + constructor(colorEnabled) { this.colorEnabled = colorEnabled; this.term = termkit.terminal; this.term.windowTitle('Netrisse'); this.screen = new termkit.ScreenBuffer({ dst: this.term, noFill: true }); - this.seed = seed; - this.interval = interval; - this.term.hideCursor(); - - if (this.colorEnabled) { - this.screen.fill({ attr: { bgColor: 'black' } }); - } - else { - this.screen.fill({ attr: { bgDefaultColor: true } }); - } - - this.d(0, 0, `Netrisse ${packageJSON.version} (C) 2016 Chris de Almeida "netrisse -h" for more info`); - this.term.grabInput(); + } - this.d(24, 19, `Seed: ${this.seed}`); - this.d(24, 20, `Speed: ${this.interval}ms`); + showGameInfo(seed, speed) { + this.d(24, 19, `Seed: ${seed}`); + this.d(24, 20, `Speed: ${speed}ms`); this.displayTime(); this.timeDisplayTimeout = setTimeout(this.displayTime.bind(this), 1000); @@ -34,16 +23,12 @@ module.exports = class Screen { * draw */ d(x, y, content, { color = 'white', bgColor = 'black' } = { color: 'white', bgColor: 'black' }) { - if (this.colorEnabled) { - this.screen.put({ x, y, attr: { color, bgColor } }, content); - } - else { - this.screen.put({ x, y }, content); - } + const attr = this.colorEnabled ? { color, bgColor } : {}; + this.screen.put({ x, y, attr }, content); } render() { - this.screen.draw(); + this.screen.draw({ delta: false }); } get(...args) { @@ -67,4 +52,10 @@ module.exports = class Screen { this.render(); } + + clear() { + const attr = this.colorEnabled ? { bgColor: 'black' } : { bgDefaultColor: true }; + this.screen.fill({ attr, region: this.writableArea }); + this.d(0, 0, `Netrisse ${packageJSON.version} (C) 2016 Chris de Almeida "netrisse -h" for more info`); + } }; diff --git a/src/start.js b/src/start.js new file mode 100644 index 0000000..0485485 --- /dev/null +++ b/src/start.js @@ -0,0 +1,145 @@ +const Board = require('./board'); +const Game = require('./game'); +const directions = require('./directions'); +const algorithms = require('./algorithms'); + +const quit = require('./quit'); +const { debug, messageTypeEnum } = require('netrisse-lib'); // eslint-disable-line no-unused-vars +const withResolvers = require('promise.withresolvers'); +withResolvers.shim(); + +module.exports = async function(seed, screen, mainBoardPosition, speed, client) { + let thisPlayerIsPaused = false; + let thisPlayerID = 0; + + let players = 1; + players += 1; + + let promiseSeed, resolveSeed, rejectSeed; + + screen.clear(); + + if (client) { + thisPlayerID = client.playerID; + + // 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 => { + // 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 messageTypeEnum.CONNECT: + { + seed = await promiseSeed; + + for (const p of message.players) { + if (p !== thisPlayerID) { + const xOffset = 1; + const boardPosition = { + top: mainBoardPosition.top, + right: (mainBoardPosition.right * 3) + xOffset, + bottom: mainBoardPosition.bottom, + left: (mainBoardPosition.right * 2) + xOffset, + }; + const b = new Board(boardPosition, screen, game, seed); + game.boards.push(b); + game.pause(true, p); + } + } + + break; + } + case messageTypeEnum.DIRECTION: + game.boards[1].currentShape.move(message.direction); + break; + case messageTypeEnum.GAME_OVER: + game.gameOver(); + quit(screen, mainBoardPosition, game, client); + break; + case messageTypeEnum.HOLD: + game.boards[1].holdShape(); + break; + case messageTypeEnum.JUNK: + { + const b = game.boards.find(b2 => b2.playerID === message.toPlayerID); + b.receiveJunk(message.junkLines); + break; + } + case messageTypeEnum.PAUSE: + game.pause(true, message.playerID); + break; + case messageTypeEnum.QUIT: + game.boards[1].quit(); + break; + case messageTypeEnum.SEED: + resolveSeed(message.seed); + break; + case messageTypeEnum.UNPAUSE: + game.pause(false, message.playerID); + break; + default: + throw new Error(`unsupported message type: ${message.type}`); + } + }); + seed = await promiseSeed; + } + + screen.seed = seed; + screen.showGameInfo(seed, speed); + const game = new Game(speed, algorithms.frustrationFree, client, thisPlayerID); + const board = new Board(mainBoardPosition, screen, game, seed); + + game.boards.push(board); + + if (players >= 1) { + // for a multi-player game, pause at the start to allow players to join + // for a single player game, also pause at the start... + thisPlayerIsPaused = true; + game.pause(true, thisPlayerID); + } + + screen.term.on('key', name => { + // the called methods should send the necessary message to the server, as there is no point in sending if it's a no-op (quick return) + switch (name) { + case 'j': case 'J': case 'LEFT': + board.currentShape?.move(directions.LEFT); + break; + case 'k': case 'K': case 'UP': + board.currentShape?.move(directions.ROTATE_LEFT); + break; + case 'l': case 'L': case 'RIGHT': + board.currentShape?.move(directions.RIGHT); + break; + case ' ': + board.currentShape?.move(directions.DROP); + break; + case 'm': case 'M': case 'DOWN': + board.currentShape?.move(directions.DOWN); + break; + case 'h': case 'H': + board.holdShape(); + break; + case 'p': case 'P': + { + thisPlayerIsPaused = !thisPlayerIsPaused; + const messageType = thisPlayerIsPaused ? messageTypeEnum.PAUSE : messageTypeEnum.UNPAUSE; + client?.sendMessage(messageType, {}); + game.pause(thisPlayerIsPaused, thisPlayerID); + break; + } + case 'CTRL_C': case 'ESCAPE': case 'Q': case 'q': + quit(screen, mainBoardPosition, game, client); + break; + default: + break; + } + }); + + function writeDebugInfo() { // eslint-disable-line no-unused-vars + console.log(`seed: ${seed}`); + console.log(JSON.stringify(board.moves)); + } +};