...
 
Commits (8)
  • Terence Martin's avatar
    Fix debug wipe · f887e42e
    Terence Martin authored
    This one has been bugging me for a while; debug wipe of the maze does
    not reset the teleports since we modified the generation code. They
    keep their destinations but are not in the maze content, so you can't
    delete them.
    f887e42e
  • Terence Martin's avatar
    Include generation option to use half balls · 028b7eec
    Terence Martin authored
    When this is enabled, only every other ball is generated into the top
    of the maze, instead of a ball in every column. This makes for an
    extra short game.
    028b7eec
  • Terence Martin's avatar
    Include values for tracking rounds · b9fdd5b2
    Terence Martin authored
    This introduces a current round and maximum round to the game state,
    along with methods for resetting the game to a new start for a given
    number of levels, advancing levels, and seeing if it is the end of the
    game or not.
    
    This works such that a maximum level count of 0 or smaller counts as a
    single round game played with half balls. Any game that is more than a
    single round will report the last round condition when it's the last
    round, which can be used to turn on the automatic arrows in maze
    generation.
    b9fdd5b2
  • Terence Martin's avatar
    Include new states for round bookends · ce3f0bb8
    Terence Martin authored
    This includes new states for the beginning and ending of a round, so
    that we can implement special logic to happen in those cases.
    ce3f0bb8
  • Terence Martin's avatar
    Implement full rounds in Game state · 04943718
    Terence Martin authored
    Now when we enter the game state, we start in the begin round state,
    ensure that everything is reset, and then proceed into the game.
    
    The game starts in the begin round state, which checks to see if the
    game is over and either goes to the maze generation for this round or
    counts this as game over.
    
    At the end of the final ball drop we go to the end of round state,
    where we update the current round and then jump to the begin round
    state.
    
    This also makes sure that the automatic arrows generate during maze
    generation if this is the last round of a multi-round game.
    04943718
  • Terence Martin's avatar
    Log what kind of game is starting · 4053715d
    Terence Martin authored
    Just to make our debugging lives easier, include a log that allows us
    to see exactly what kind of game we're into.
    4053715d
  • Terence Martin's avatar
    Include Menu and Pointer entities · e1ac9f03
    Terence Martin authored
    These are shamelessly ripped directly from Devember 2015. You know
    you do it too.
    e1ac9f03
  • Terence Martin's avatar
    Include a Title Screen · f512c9e8
    Terence Martin authored
    Yeah, this is also a rip from the Devember 2015 title screen, but it
    has the menu that does the stuff, so that's pretty good.
    f512c9e8
This diff is collapsed.
......@@ -17,6 +17,25 @@ module nurdz.game
*/
export const BALL_POSITION_MULTIPLIER = 2;
/**
* True if we should generate a level with only half of the normal number
* of balls. This will only generate a ball into the even numbered columns
* in the maze. This makes for a shorter game.
*/
export let halfBalls : boolean = false;
/**
* The current round number in the game. This advances every time the last
* ball is dropped at the end of a round.
*/
export let currentRound : number = 1;
/**
* The maximum number of rounds in the game. Once the current round exceeds
* this value, the game is officially over.
*/
export let maxRounds : number = 1;
/**
* The number of points the human player has.
*/
......@@ -27,6 +46,76 @@ module nurdz.game
*/
let computerScore : number = 0;
/**
* Set up a new game to be played over the given number of total rounds. A
* value of 0 or smaller means that we will be playing only a single round,
* but with half balls.
*
* This will set the current round to 1 and reset the scores.
*
* @param {number} totalRounds the total number of rounds; can be 0 to
* indicate a 1 round game played with half balls.
*/
export function newGame (totalRounds : number) : void
{
// Start at round one and store the total rounds given. When the total
// rounds is 0 or smaller, assume 1.
currentRound = 1;
maxRounds = (totalRounds > 0) ? totalRounds : 1;
// We want to use half balls only when total rounds is 0 or smaller.
halfBalls = (totalRounds <= 0) ? true : false;
// Start the game with empty scores.
resetScores ();
// Log what's happening.
console.log(
String.format (
"DEBUG: Starting a new game with round count of {0} and {1} half balls",
maxRounds,
(halfBalls ? "with" : "without")
));
}
/**
* Skip the round counter to indicate that we're in the next round now.
*/
export function nextRound () : void
{
currentRound++;
}
/**
* Check to see if we think the game should be over right now. This is based
* purely on the current round number, so this should only be checked after
* modifying that value.
*
* @returns {boolean} true if the game is now over, false otherwise
*/
export function isGameOver () : boolean
{
return currentRound > maxRounds;
}
/**
* Return an indication as to whether this is the last round of the game or
* not. This always returns false if the maximum number of rounds is not
* greater than 1, because the use of this function is for determing last
* round setup, which only happens for a game longer than one round.
*
* @returns {boolean} true if this is the last round
*/
export function isLastRound () : boolean
{
// Never the last round for a single round game
if (maxRounds == 1)
return false;
// This is the last round when we meet or exceed the last round.
return currentRound >= maxRounds;
}
/**
* Reset the score values for both players.
*/
......
......@@ -146,6 +146,10 @@ module nurdz.game
// Clear all cells.
this._contents.clearCells ();
// Clear all destinations on the existing teleport.
if (this._teleport)
this._teleport.clearDestinations ();
// Now the left and right sides need to be solid bricks.
for (let y = 0 ; y < MAZE_HEIGHT ; y++)
{
......@@ -467,8 +471,11 @@ module nurdz.game
* Currently this fill up the top row with balls for the player only,
* but it should also store balls for the computer into another data
* structure.
*
* @param {boolean} halfBalls true if we should generate half of the
* usual number of balls, to make for a shorter game.
*/
private placeBalls () : void
private placeBalls (halfBalls : boolean) : void
{
// Get the arrays that store the player and comptuer balls from
// the contents object.
......@@ -482,6 +489,11 @@ module nurdz.game
// balls for our purposes.
for (let ballIndex = 0 ; ballIndex < MAZE_WIDTH - 2 ; ballIndex++)
{
// If we are rendering half balls and this is not an even numbered
// column, skip it.
if (halfBalls && ballIndex % 2 != 0)
continue;
// Get the balls from the pool
playerBalls[ballIndex] = this._maze.getBall ();
computerBalls[ballIndex] = this._maze.getBall ();
......@@ -511,10 +523,10 @@ module nurdz.game
* us, but does not take care to reap any objects in the pools first;
* that is up to the caller.
*
* @param includeAutomatic true if arrows can be generated as
* automatically flipping, or false otherwise.
* @param {boolean} halfBalls true if half the usual number of balls should be generated per player
* @param {boolean} includeAutomatic true if arrows should be generated as automatically flipping
*/
generate (includeAutomatic : boolean) : void
generate (halfBalls : boolean, includeAutomatic : boolean) : void
{
// Empty the maze of all of its contents.
this.emptyMaze ();
......@@ -526,7 +538,7 @@ module nurdz.game
this.genBonusBricks ();
// Now we can place the balls in.
this.placeBalls ();
this.placeBalls (halfBalls);
}
}
}
\ No newline at end of file
......@@ -12,6 +12,13 @@ module nurdz.game
*/
NO_STATE,
/**
* This state is set when it's the start of a new round. This checks to
* see if the game is over or not, and either proceeds to generation of
* a new maze or goes to the end of the game state.
*/
BEGIN_ROUND,
/**
* This state is set while the maze is undergoing generation. The Maze
* entity doing the generation will inform the registered event listener
......@@ -85,6 +92,13 @@ module nurdz.game
*/
FINAL_BALL_DROP,
/**
* We were dropping final balls, but we have determined that there are
* no more balls to drop. In this case we advance to the next round of
* the game.
*/
END_ROUND,
/**
* All of the gray bricks have been removed, all of the final ball drops
* have finished, and we have no more rounds to play; the game is just
......
......@@ -1253,8 +1253,11 @@ module nurdz.game
this._ballMoveFinalized = false;
this._droppingFinalBall = false;
// Now generate the contents of the maze.
this._generator.generate (true);
// Now generate the contents of the maze. The indication for the use
// of half balls comes from when the game was set that way at start
// time. On the last round of a multi round game, we generate
// automatic arrows too.
this._generator.generate (halfBalls, isLastRound ());
// Reset the scores
resetScores ();
......
module nurdz.game
{
/**
* A simple menu item structure.
*/
export interface MenuItem
{
/**
* The position of this menu item; the text starts at an offset to the right, this specifies where
* the menu pointer goes.
*/
position: Point;
/**
* The displayed menu text.
*/
text: string;
}
/**
* This class represents a menu. This is responsible for rendering menu items and handling key input
* while a menu is active.
*/
export class Menu extends Actor
{
/**
* The name of the font as given in the constructor.
*/
private _fontName : string;
/**
* The size of our font, in pixels.
*/
private _fontSize : number;
/**
* The full font string specification; this is a combination of the font name and size.
*/
private _fontFullSpec : string;
/**
* The list of menu items that we contain.
*/
private _items : Array<MenuItem>;
/**
* The currently selected menu item in our item array. This item is visually distinguished from
* other menu items.
*/
private _selected : number;
/**
* The menu pointer. We always position it so that it marks what the currently selected menu item is.
*/
private _pointer : Pointer;
/**
* The sound to play when the menu selection changes; this can be null, in which case no sound is
* played.
*/
private _selectSound : Sound;
/**
* Return the menu item index at the currently selected location.
*
* @returns {number}
*/
get selected () : number
{ return this._selected; }
/**
* Return the number of items that are currently in the menu.
*
* @returns {number}
*/
get length () : number
{ return this._items.length; }
/**
* Construct a new menu which renders its menu text with the font name and size provided.
*
* @param stage the stage that will display this menu
* @param fontName the font name to render the menu with
* @param fontSize the size of the font to use to render items, in pixels
* @param sound the sound to play when the menu selection changes.
*/
constructor (stage : Stage, fontName : string, fontSize : number, sound : Sound = null)
{
// Simple super call. We don't have a visual position per se.
super ("Menu", stage, 0, 0, 0, 0);
// Store the values provided.
this._fontName = fontName;
this._fontSize = fontSize;
this._selectSound = sound;
// Combine them together into a single string for later use
this._fontFullSpec = this._fontSize + "px " + this._fontName;
// The menu starts out empty.
this._items = [];
this._selected = 0;
// Set up the pointer.
this._pointer = new Pointer (stage, 0, 0);
}
/**
* Change the location of the menu pointer to point to the currently selected menu item.
*/
private updateMenuPointer () : void
{
if (this._items.length > 0)
this._pointer.setStagePositionXY (this._items[this._selected].position.x,
this._items[this._selected].position.y);
}
/**
* Add a new menu item to the list of menu items managed by this menu instance.
*
* @param text the text of the menu item
* @param position the position on the screen of this item.
*/
addItem (text : string, position : Point)
{
// Insert the menu item
this._items.push (
{
text: text,
position: position
});
// If the current length of the items array is now 1, the first item is finally here, so
// position our pointer.
if (this._items.length == 1)
this.updateMenuPointer ();
}
/**
* Return the menu item at the provided index, which will be null if the index provided is out of
* range of the number of items currently maintained in the menu.
*
* @param index the index of the item to get
* @returns {MenuItem} the menu item at the provided index or null if the index is not valid.
*/
getItem (index : number) : MenuItem
{
if (index < 0 || index >= this._items.length)
return null;
return this._items[index];
}
/**
* Change the selected menu item to the previous item, if possible.
*
* If a sound is associated with the menu, it will be played.
*/
selectPrevious () : void
{
this._selected--;
if (this._selected < 0)
this._selected = this._items.length - 1;
this.updateMenuPointer ();
if (this._selectSound != null)
this._selectSound.play ();
}
/**
* Change the selected menu item to the next item, if possible.
*
* If a sound is associated with the menu, it will be played.
*/
selectNext () : void
{
this._selected++;
if (this._selected >= this._items.length)
this._selected = 0;
this.updateMenuPointer ();
if (this._selectSound != null)
this._selectSound.play ();
}
/**
* Update the state of the menu based on the current tick; we use this to visually mark the
* currently selected menu item.
*
* @param stage the stage that owns us
* @param tick the current update tick
*/
update (stage : nurdz.game.Stage, tick : number) : void
{
// Make sure our pointer updates
this._pointer.update (stage, tick);
}
/**
* Render ourselves using the provided renderer. This will render out the text as well as the
* current pointer.
*
* The position provided to us is ignored; we already have an idea of where exactly our contents
* will render.
*/
render (x : number, y : number, renderer : CanvasRenderer) : void
{
// Render the pointer at its current position.
this._pointer.render (this._pointer.position.x, this._pointer.position.y, renderer);
// Save the context and set up our font and font rendering.
renderer.context.save ();
renderer.context.font = this._fontFullSpec;
renderer.context.textBaseline = "middle";
// Render all of the text items. We offset them by the width of the pointer that indicates
// which item is the current item, with a vertical offset that is half of its height. This
// makes the point on the pointer align with the center of the text.
for (let i = 0 ; i < this._items.length ; i++)
{
let item = this._items[i];
renderer.drawTxt (item.text, item.position.x + TILE_SIZE,
item.position.y + (TILE_SIZE / 2), 'white');
}
renderer.restore ();
}
}
}
module nurdz.game
{
/**
* The properties that a capsule can have.
*/
interface PointerProperties extends EntityProperties
{
/**
* When true, we render ourselves when asked; otherwise we silently ignore render calls.
*/
visible? : boolean;
/**
* The rotation of the pointer, in degrees. The default orientation corresponds to a rotation
* angle of 0, which points the pointer to the right. 90 degrees is facing down.
*/
rotation? : number;
}
/**
* This entity represents a simplistic pointer, which is just a tile sized entity that appears to
* slowly flash and points downwards. It's used for our debug logic.
*/
export class Pointer extends Entity
{
/**
* Redeclare our pointer properties so that it is of the correct type. This is allowed because the
* member is protected.
*/
protected _properties : PointerProperties;
get properties () : PointerProperties
{ return this._properties; }
/**
* The index into the color list that indicates what color to render ourselves.
*
* @type {number}
*/
private _colorIndex : number = 0;
/**
* The list of colors that we use to display ourselves.
*
* @type {Array<string>}
*/
private _colors : Array<string> = ['#ffffff', '#aaaaaa'];
/**
* The polygon that represents us.
*
* @type {Polygon}
*/
private _poly : Polygon = [
[-(TILE_SIZE / 2) + 4, -(TILE_SIZE / 2) + 4],
[(TILE_SIZE / 2) - 4, 0],
[-(TILE_SIZE / 2) + 4, (TILE_SIZE / 2) - 4],
];
/**
* Create the pointer object to be owned by the stage.
*
* @param stage the stage that owns this pointer
* @param x the X location of the pointer
* @param y the Y location of the pointer`
* @param rotation the rotation of the pointer initially.
*/
constructor (stage : Stage, x : number, y : number, rotation : number = 0)
{
super ("Cursor", stage, x, y, TILE_SIZE, TILE_SIZE, 1, <PointerProperties> {
visible: true,
rotation: rotation
});
}
/**
* Called every frame to update ourselves. This causes our color to change.
*
* @param stage the stage that the actor is on
* @param tick the game tick; this is a count of how many times the game loop has executed
*/
update (stage : Stage, tick : number) : void
{
if (tick % 7 == 0)
{
this._colorIndex++;
if (this._colorIndex == this._colors.length)
this._colorIndex = 0;
}
}
/**
* Render ourselves as an arrow rotated in the direction that we are rotated for.
*
* @param x the X location of where to draw ourselves
* @param y the Y location of where to draw ourselves
* @param renderer the renderer to use to draw ourselves
*/
render (x : number, y : number, renderer : Renderer) : void
{
// Only render if we're visible.
if (this._properties.visible)
{
// Get ready for rendering. The X, Y we get is our upper left corner but in order to
// render properly we need it to be our center.
renderer.translateAndRotate (x + (TILE_SIZE / 2), y + (TILE_SIZE / 2),
this._properties.rotation);
renderer.fillPolygon (this._poly, this._colors[this._colorIndex]);
renderer.restore ();
}
}
}
}
......@@ -78,9 +78,10 @@ module nurdz.main
// Register all of our scenes.
stage.addScene ("game", new game.GameScene (stage));
stage.addScene ("title", new game.TitleScreen (stage));
// Switch to the initial scene, add a dot to display and then run the game.
stage.switchToScene ("game");
stage.switchToScene ("title");
stage.run ();
}
catch (error)
......
......@@ -162,9 +162,14 @@ module nurdz.game
this._player.referencePoint = this._maze.position.copyTranslatedXY (0, -this._maze.cellSize);
this._computer.referencePoint = this._maze.position.copyTranslatedXY (0, -this._maze.cellSize);
// If there is no current state, it's time to generate a new level.
if (this.state == GameState.NO_STATE)
this.state = GameState.MAZE_GENERATION;
// When we become active, we're always going to start a new game,
// so make sure that the maze is clear, our player entities are
// hidden, and then set our state to the begin round state.
this._player.visible = false;
this._computer.visible = false;
this._maze.resetMazeEntities ();
this._maze.generator.emptyMaze ();
this.state = GameState.BEGIN_ROUND;
}
/**
......@@ -677,6 +682,16 @@ module nurdz.game
// Handle based on state.
switch (this.state)
{
// It is the start of a new round; if the game is over now,
// switch to the game over state. Otherwise, we can swap to the
// maze generation state for this round.
case GameState.BEGIN_ROUND:
if (isGameOver ())
this.state = GameState.GAME_OVER;
else
this.state = GameState.MAZE_GENERATION;
break;
// It is becoming the player's turn; check to see if there is
// a valid play for them; if yes, make it their turn. Otherwise,
// make it the computer turn.
......@@ -748,7 +763,15 @@ module nurdz.game
// automatically.
case GameState.FINAL_BALL_DROP:
if (this._maze.dropNextFinalBall () == false)
this.state = GameState.GAME_OVER;
this.state = GameState.END_ROUND;
break;
// It's the end of the round; go to the next round and then skip
// back to the begin round code, which will see what we should
// be doing.
case GameState.END_ROUND:
nextRound ();
this.state = GameState.BEGIN_ROUND;
break;
}
}
......
module nurdz.game
{
/**
* The font that is used for the title font.
*/
const TITLE_FONT = "96px Arial,Serif";
/**
* The font that is used for our informative text.
*/
const INFO_FONT = "32px Arial,Serif";
/**
* The font that is used to display our menu text.
*/
const MENU_FONT = "40px Arial,Serif";
/**
* This class represents the title screen. It allows the user to select the level that the game will
* be played at.
*/
export class TitleScreen extends Scene
{
/**
* As a supreme hack, redefine the property that defines our renderer so
* that the compiler knows that it is a canvas renderer. This allows us
* to get at its context so we can do things outside of what the current
* API allows for without having to noodle with the API more.
*/
protected _renderer : CanvasRenderer;
/**
* The list of menu items that we display to the user.
*/
private _menu : Menu;
/**
* The number of rounds we think the game should start with; 0 means a
* short game, 1 is a regular game, 3 is a long game.
*/
private _totalRounds : number;
/**
* Construct the title screen scene.
*
* @param stage the stage that controls us
*/
constructor (stage : Stage)
{
// Let the super do some setup
super ("titleScreen", stage);
// Set up our menu.
this._menu = new Menu (stage, "Arial,Serif", 40);
this._menu.addItem ("Game type: Short", new Point (150, 400));
this._menu.addItem ("Start Game", new Point (150, 450));
// Make sure it gets render and update requests.
this.addActor (this._menu);
// default level.
this._totalRounds = 0;
}
/**
* Get the name for the type of game that has the current number of
* rounds we're currently set to.
*
* @returns {string} the game type for this number of rounds.
*/
private gameTypeForRounds () : string
{
switch (this._totalRounds)
{
case 0:
return "Short";
case 1:
return "Normal";
default:
return "Long";
}
}
/**
* This helper updates our menu to show what the currently selected level is.
*/
private updateMenu () : void
{
let item = this._menu.getItem (0);
if (item)
item.text = "Game type: " + this.gameTypeForRounds ();
}
/**
* Render the name of the game to the screen.
*/
private renderTitle () : void
{
this._renderer.translateAndRotate (this._stage.width / 2, 45);
// Set the font and indicate that the text should be centered in both directions.
this._renderer.context.font = TITLE_FONT;
this._renderer.context.textAlign = "center";
this._renderer.context.textBaseline = "middle";
// Draw the text and restore the context.
this._renderer.drawTxt ("A-Maze-Balls", 0, 0, 'white');
this._renderer.restore ();
}
/**
* Render our info text to the screen.
*/
private renderInfoText () : void
{
// The info text that we generate to the screen to explain what we are.
const infoText = [
"A simple Bolo Ball clone",
"",
"Coded during #devember 2016 by Terence Martin",
"for game development practice",
"",
"Feel free to use this code as you see fit. See the",
"LICENSE file for details"
];
// Save the context state and then set our font and vertical font alignment.
this._renderer.translateAndRotate (TILE_SIZE, 132);
this._renderer.context.font = INFO_FONT;
this._renderer.context.textBaseline = "middle";
// Draw the text now
for (let i = 0, y = 0 ; i < infoText.length ; i++, y += TILE_SIZE)
this._renderer.drawTxt (infoText[i], 0, y, '#c8c8c8');
// We can restore now.
this._renderer.restore ();
}
/**
* Invoked to render us. We clear the screen, show some intro text, and we allow the user to
* select a starting level.
*/
render () : void
{
// Clear the screen and render all of our text.
this._renderer.clear ('black');
this.renderTitle ();
this.renderInfoText ();
// Now let the super draw everything else, including our menu
super.render ();
}
/**
* Start a new game using the currently set total number of rounds.
*/
private startGame () : void
{
// First, set up for a new game of a set number of levels. Due to
// the hacky nature of this, the short and normal games have the
// right number of rounds, but the long game has 2 rounds instead of
// 3 due to design decisions made not by me.
newGame (this._totalRounds != 2 ? this._totalRounds : 3);
this._stage.switchToScene ("game");
}
/**
* Triggers on a key press
*
* @param eventObj key event object
* @returns {boolean} true if we handled the key or false otherwise.
*/
inputKeyDown (eventObj : KeyboardEvent) : boolean
{
// If the super handles the key, we're done.
if (super.inputKeyDown (eventObj))
return true;
switch (eventObj.keyCode)
{
// Previous menu selection (wraps around)
case KeyCodes.KEY_UP:
this._menu.selectPrevious ();
return true;
// Next menu selection (wraps around)
case KeyCodes.KEY_DOWN:
this._menu.selectNext ();
return true;
// Change the game type
case KeyCodes.KEY_LEFT:
if (this._menu.selected == 0)
{
if (this._totalRounds > 0)
{
this._totalRounds--;
this.updateMenu ();
}
return true;
}
return false;
// Change the game type.
case KeyCodes.KEY_RIGHT:
if (this._menu.selected == 0)
{
if (this._totalRounds < 2)
{
this._totalRounds++;
this.updateMenu ();
}
return true;
}
return false;
// Select the current menu item; on level increase it, on start
// game, do that.
case KeyCodes.KEY_ENTER:
if (this._menu.selected == 0)
{
this._totalRounds++;
if (this._totalRounds > 2)
this._totalRounds = 0;
this.updateMenu ();
return true;
}
else
{
this.startGame ();
return true;
}
}
// Not handled.
return false;
}
}
}
......@@ -16,6 +16,8 @@
"MazeDebugger.ts",
"ComputerAI.ts",
"StateMachine.ts",
"entity/Pointer.ts",
"entity/Menu.ts",
"entity/Player.ts",
"entity/MazeCell.ts",
"entity/Marker.ts",
......@@ -24,6 +26,7 @@
"entity/Teleport.ts",
"entity/Arrow.ts",
"entity/Maze.ts",
"scene/Title.ts",
"scene/Game.ts",
"main.ts"
]
......