commit 4830e50375442434b924f2b3fb4becdd924017bf Author: Mahdi Dibaiee Date: Fri Dec 11 17:47:45 2015 +0330 chore: add spacecraft image diff --git a/README.md b/README.md new file mode 100644 index 0000000..dac0c59 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +node-games +========== + +Some fun console games written using Node. + +spacecraft +---------- +![spacecraft](https://raw.githubusercontent.com/mdibaiee/node-games/master/spacecraft.png) diff --git a/classes/interface.js b/classes/interface.js new file mode 100644 index 0000000..92cb0c8 --- /dev/null +++ b/classes/interface.js @@ -0,0 +1,55 @@ +import ansi from 'ansi'; +const { stdout, stdin } = process; + +let listeners = []; + +const prefix = '\u001b'; +const keys = { + right: `${prefix}[C`, + up: `${prefix}[A`, + left: `${prefix}[D`, + down: `${prefix}[B`, + exit: '\u0003', + space: ' ' +} + +export default class Interface { + constructor(output = stdout, input = stdin) { + this.output = output; + this.input = input; + + this.input.setRawMode(true); + this.input.setEncoding('utf8'); + + this.cursor = ansi(this.output).hide(); + + this.columns = this.output.columns; + this.rows = this.output.rows; + + this.input.addListener('data', data => { + let key = Object.keys(keys).find((value, i) => { + return keys[value] === data; + }); + + if ( key === 'exit' ) process.exit(); + + let match = listeners.filter(listener => { + return listener.key === key || listener.key === data; + }); + + match.forEach(listener => listener.fn()); + }) + } + + clear() { + this.output.write('\u001b[2J\u001b[0;0H'); + } + + write(...args) { + this.cursor.write(...args); + } + + onKey(key, fn) { + listeners.push({ key, fn }); + } +} diff --git a/classes/unit.js b/classes/unit.js new file mode 100644 index 0000000..31fdb66 --- /dev/null +++ b/classes/unit.js @@ -0,0 +1,91 @@ +export default class Unit { + constructor(output) { + this._health = 0; + this.dead = false; + this.output = output; + + this.dieOnExit = false; + } + + set health(value) { + this._health = Math.min(0, value); + + if (this.health === 0) { + this.dead = true; + } + } + + get health() { + return this._health; + } + + set x(value) { + this._x = value; + + if (this.dieOnExit && this._x > this.output.columns) { + this.dead = true; + } + } + + get x() { + return this._x; + } + + set y(value) { + this._y = value; + + if (this.dieOnExit && this._y > this.output.rows) { + this.dead = true; + } + } + + get y() { + return this._y; + } + + move(x, y) { + if (!x && !y) return this.move(...this.speed()); + this.x += x; + this.y += y; + } + + draw() { + if (this.dead) return; + + let { x, y, shape } = this; + if (this.color && this.color[0] === '#') { + this.output.cursor.hex(this.color); + } else if (this.color) { + this.output.cursor[this.color](); + } + if (this.bold) this.output.cursor.bold(); + + + this.output.cursor.goto(Math.round(x), Math.round(y)); + this.output.write(shape); + this.output.cursor.reset(); + } + + go(x, y) { + this.x = x; + this.y = y; + } + + speed() { + let signs = [Math.random() > 0.5 ? -1 : 1, Math.random() > 0.5 ? -1 : 1]; + return [Math.random() * signs[0], Math.random() * signs[1]]; + } + + collides(target) { + if (Array.isArray(target)) { + return target.find(t => this.collides(t)); + } + + let targetX = Math.round(target.x); + let targetY = Math.round(target.y); + let x = Math.round(this.x); + let y = Math.round(this.y); + + return x === targetX && y == targetY; + } +} diff --git a/spacecraft.js b/spacecraft.js new file mode 100644 index 0000000..bc1ab2c --- /dev/null +++ b/spacecraft.js @@ -0,0 +1,103 @@ +import Unit from './classes/unit'; +import Interface from './classes/interface'; + +const FRAME = 20; +const ENEMY_SPAWN_RATE = 400; + +let ui = new Interface(); + +let player = new Unit(ui); +player.go(1, ui.output.rows / 2); +player.shape = '=>'; +player.color = '#77d6ff'; +player.bold = true; + +let explosion = new Unit(ui); +explosion.dead = true; + +let missles = []; +let enemies = []; +let score = 0; +setInterval(() => { + ui.clear(); + + player.draw(); + + missles.forEach((missle, i) => { + missle.move(1, 0); + missle.draw(); + + let enemy = missle.collides(enemies) + if (enemy) { + enemy.killed = 1; + enemy.color = 'red'; + enemy.shape = '*'; + missle.dead = true; + + score++; + } + + if (missle.dead) { + missles.splice(i, 1); + } + }); + + enemies.forEach((enemy, i) => { + // move with speed + enemy.move(); + enemy.draw(); + + if (enemy.dead) { + enemies.splice(i, 1); + } + + if (enemy.killed == 3) enemy.dead = true; + if (enemy.killed < 3) enemy.killed++; + }) + + ui.cursor.goto(0, 0).yellow().write(`Score: ${score}`); + ui.cursor.reset(); +}, FRAME); + + +ui.onKey('right', () => { + player.move(1, 0); +}); +ui.onKey('down', () => { + player.move(0, 1); +}); +ui.onKey('up', () => { + player.move(0, -1); +}); +ui.onKey('left', () => { + player.move(-1, 0); +}); +ui.onKey('space', () => { + let missle = new Unit(ui); + missle.go(player.x, player.y); + missle.shape = '+'; + missle.dieOnExit = true; + + missles.push(missle); +}); + +setInterval(() => { + if (enemies.length > 5) return; + + let enemy = new Unit(ui); + enemy.go(Math.random() * ui.output.columns, 0); + enemy.shape = 'o'; + enemy.color = '#f7c71e'; + enemy.dieOnExit = true; + + enemy.speed = () => { + return [Math.random() > 0.9 ? 0.4 : 0, 0.06]; + } + + enemies.push(enemy); +}, 1000); + +process.on('exit', function() { + ui.cursor.horizontalAbsolute(0).eraseLine() + ui.cursor.show(); +}); diff --git a/spacecraft.png b/spacecraft.png new file mode 100644 index 0000000..a007b1e Binary files /dev/null and b/spacecraft.png differ