Skip to content

Instantly share code, notes, and snippets.

@micolous
Last active July 23, 2022 02:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save micolous/b822a40674e5527aba42f0612a013faf to your computer and use it in GitHub Desktop.
Save micolous/b822a40674e5527aba42f0612a013faf to your computer and use it in GitHub Desktop.
Flappy Birb (for Last Call BBS)
"use strict";
/**
* @file Flappy Birb (for Last Call BBS)
* @version 1.2
* @copyright 2022 Michael Farrell
* @author Michael Farrell <micolous+git@gmail.com>
* @license Apache-2.0
*
* Changes:
*
* v1.2 (2022-07-23):
* - Remove undeeded "initialSave"
* - Handle birb collision with the rounded y-pos, so that a rendered collision
* (with fractional offset) will always be treated as a true collision
* - Slowly increase speed as the game progresses
* - Adds a cheat code
*
* v1.1 (2022-07-23):
* - Code cleanups and documentation
* - Fix wall rendering issue for the bottom of the screen
* - Initial release on Reddit
*
* v1.0 (2022-07-22):
* - Initial version
*/
/**
* Default speed at which walls move left, in milliseconds.
*/
const DEFAULT_SPEED = 300;
/**
* Maximum speed at which walls move left, in milliseconds.
* We can't go too fast, because otherwise the screen can't update fast enough.
*/
const MAX_SPEED = 150;
/**
* Current speed at which walls move left, in milliseconds.
* @type {number}
*/
let speed = DEFAULT_SPEED;
/**
* The Y position of the birb.
* @type {number}
*/
let birb_y;
/**
* If the game is paused.
* @type {boolean}
*/
let paused;
/**
* Time that the jump key was last pressed, in milliseconds since epoch.
* This is used to give a parabolic motion.
* When the game starts, this is set to 400ms before now.
* @type {number}
*/
let last_jump;
/**
* Time that the walls last moved, in milliseconds since epoch.
* @type {?number}
*/
let last_wall_move;
/**
* Time that the game started, in milliseconds since epoch.
* Null if no game has been started.
* @type {?number}
*/
let game_start_time;
/**
* Time that the game ended, in milliseconds since epoch.
* Null if a game is in progress, or a game has not started yet.
* @type {?number}
*/
let game_end_time;
/**
* Gaps in the current playfield, used to describe the walls.
*
* An array with up to 55 elements (x co-ordinate), containing a 2-value array
* describing the gap in the wall (y co-ordinate, height).
* @type {number[][]}
*/
let walls = [];
/**
* Show introduction text when first dialled in.
* @type {boolean}
*/
let intro = true;
/**
* The current "save game".
* @see {@link load} to load state from disk
* @see {@link save} to save this state to disk
* @property {number} highScore The current high score
* @property {number} plays The number of games started
*/
let saveGame = {
highScore: 0,
plays: 0
};
const CHEAT_CODE = 'iddqd';
let cheat_code_pos = 0;
let cheat_mode = false;
/**
* Returns the current time in milliseconds since epoch.
* @returns {number}
*/
function tsMillis() {
return (new Date()).getTime();
}
/**
* This function should return a string that will be used as the server's name.
* It must be short enough to fit in the NETronics Connect! menu.
* @returns {string}
*/
function getName() {
return 'Flappy Birb';
}
/**
* This function will be called when a user connects to the server.
*
* If you re-dial, the game will re-use your existing state, so any globals
* would otherwise keep their existing value.
*/
function onConnect() {
load();
speed = DEFAULT_SPEED;
cheat_code_pos = 0;
birb_y = 9;
paused = true;
game_start_time = null;
game_end_time = null;
intro = true;
walls = [];
generateWalls();
}
/**
* Saves {@link saveGame} to disk.
*/
function save() {
saveData(JSON.stringify(saveGame));
}
/**
* Loads {@link saveGame} from disk, or creates a new save.
*/
function load() {
let d = loadData();
if (d == '') {
save();
} else {
saveGame = JSON.parse(d);
}
}
/**
* Starts a game, but doesn't re-initialise the walls.
*/
function startGame() {
paused = false;
speed = DEFAULT_SPEED;
birb_y = 9;
game_end_time = null;
game_start_time = tsMillis();
// So we start falling, and don't jump on start.
last_jump = game_start_time - 400;
last_wall_move = game_start_time;
intro = false;
saveGame.plays++;
save();
}
/**
* Starts a new game, re-initialising the walls.
*/
function restartGame() {
walls = [];
generateWalls();
startGame();
}
/**
* Generates enough walls for the play field.
*/
function generateWalls() {
for (let x = walls.length; x < 55; x++) {
let isEmpty = false;
if (x == 54) {
// New wall, count how many empty slots we have
let emptyCount = walls.filter(function (v) { return v[0] == 1 && v[1] == 18; }).length;
if (emptyCount < (x / 11 * 10)) {
isEmpty = true;
}
} else {
isEmpty = (x % 11) < 10;
}
if (isEmpty) {
walls.push([1, 18]);
} else {
let gap_y = Math.floor((Math.random() * 10) + 1);
let gap_h = Math.ceil((Math.random() * 3) + 6);
if (gap_h + gap_y > 19) {
gap_h = 19 - gap_y;
}
walls.push([gap_y, gap_h]);
}
}
}
/**
* This function will be called approximately 30 times a
* second while a user is connected to the server.
*
* (but if your text changes too much, it'll just bail mid-frame...)
*/
function onUpdate() {
const now = tsMillis();
clearScreen();
// Screen: x=0..55, y=0..19
// Move things around when the game is active.
if (!paused && game_end_time == null) {
// For the first 400ms after a jump, the birb goes up (-y).
// Then it starts going down (+y), increasing in speed.
// The player needs to jump constantly to maintain altitude.
let jump_ago = now - last_jump;
birb_y += (jump_ago - 400) / 500;
// Walls advance forward.
let walls_ago = now - last_wall_move;
if (walls_ago >= speed) {
last_wall_move = now;
walls.shift();
generateWalls();
}
// Gradually ramp up the speed as the game progresses.
if (speed >= MAX_SPEED) {
let game_time = now - game_start_time;
speed = Math.max(MAX_SPEED, DEFAULT_SPEED - (game_time / 300));
}
}
// Render walls
walls.forEach(function (v, x) {
let c = ((55 - (x < 5 ? 5 : x)) / 50) * 10 + 2;
for (let y = 1; y <= v[0]; y++) {
drawText('█', c, x, y);
}
let ly = v[0] + v[1];
for (let y = ly; y <= 19; y++) {
drawText('█', c, x, y);
}
});
drawText('█', 2, 55, 1);
drawText('█', 2, 55, 19);
// Check for collisions.
let ry = Math.round(birb_y);
if (ry >= 19 || ry <= 1 || (!cheat_mode && walls.length > 5 && (ry <= walls[5][0] || ry >= walls[5][0] + walls[5][1]))) {
// Birb hit a wall :(
paused = true;
if (game_end_time == null) {
game_end_time = now;
}
drawText('⚉', 17, 5, ry);
} else {
// Birb is still flying
drawText('☻', 17, 5, ry);
}
if (intro) {
drawText('Press SPACE to start.', ((now % 2000) <= 1000 ? 14 : 17), 17, 10);
}
// Scoreboard
let score;
if (game_end_time != null) {
drawText('Game over. Press SPACE to retry.', ((now % 2000) <= 1000 ? 14 : 17), 11, 10);
score = Math.floor((game_end_time - game_start_time) / 1000);
} else if (game_start_time == null) {
// No game started.
score = 0;
} else {
// Game in progress.
score = Math.floor((now - game_start_time) / 1000);
if (!cheat_mode && score > saveGame.highScore) {
// Cheaters don't get high scores.
saveGame.highScore = score;
save();
}
}
if (intro || game_end_time != null) {
drawText('(c) 1992 micolous', 17, 1, 19);
drawText('v1.2', 17, 51, 19);
}
if (cheat_mode) {
drawText('CHEAT MODE ACTIVE', 17, 30, 19);
}
// Header
drawText('Flappy Birb', 17, 0, 0);
drawText('Plays: ' + saveGame.plays, 13, 15, 0);
drawText('Score: ' + score, 17, 30, 0);
drawText('Best: ' + saveGame.highScore, saveGame.highScore == score ? 17 : 13, 45, 0);
}
/**
* This function will be called every time the connected user presses a key.
* @param {number} key the ASCII representation of the key pressed
*/
function onInput(key) {
if (key == 32) {
if (paused) {
if (game_end_time == null) {
// On first connect, we don't want to reset walls
startGame();
} else {
// After that, we want to reset everything.
restartGame();
}
} else {
let now = tsMillis();
if (now - last_jump > 100) {
last_jump = now;
}
}
}
if (paused && String.fromCharCode(key) == CHEAT_CODE[cheat_code_pos]) {
cheat_code_pos++;
if (cheat_code_pos >= CHEAT_CODE.length) {
cheat_mode = !cheat_mode;
cheat_code_pos = 0;
}
} else {
cheat_code_pos = 0;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment