Maze.ts 52.8 KB
Newer Older
Terence Martin's avatar
Terence Martin committed
1 2
module nurdz.game
{
3 4 5 6 7 8 9 10 11
    /**
     * The number of ticks between steps in a normal (interactive) ball drop,
     */
    const NORMAL_DROP_SPEED = 3;

    /**
     * The number of ticks between steps in a final (end of round) ball drop.
     */
    const FINAL_DROP_SPEED = 1;
12 13 14 15 16 17 18

    /**
     * This interface is implemented by any class that wants to be told about
     * game specific events that occur as a result of operations in the maze.
     */
    export interface MazeEventListener
    {
Terence Martin's avatar
Terence Martin committed
19 20 21 22 23 24 25 26 27 28
        /**
         * The maze was asked to push a ball somehow and the ball push has
         * started now.
         *
         * @param {Ball} ball the ball entity that is being pushed.
         */
        startBallPush (ball : Ball) : void;

        /**
         * A ball drop that was in progress has now finished. The event features
29
         * the ball that was dropped.
Terence Martin's avatar
Terence Martin committed
30 31 32 33
         *
         * This gets triggered once the ball comes to a rest and before it is
         * vanished away (if it should be).
         *
34 35 36
         * This is triggered for any ball drop; human or computer, during the
         * regular game or as the final ball drop.
         *
37
         * @param {Ball} ball the ball that stopped dropping
Terence Martin's avatar
Terence Martin committed
38
         */
39
        ballDropComplete (ball : Ball) : void;
Terence Martin's avatar
Terence Martin committed
40

41 42 43 44 45 46 47 48 49 50 51 52 53 54
        /**
         * A ball that is blocked has been told that it's being removed from
         * the maze during the final part of the round just prior to the gray
         * bricks being removed and the final ball drop.
         *
         * This gets triggered once the ball has been told to vanish away but
         * before the vanish starts.
         *
         * This is triggered for both human and computer balls.
         *
         * @param {Ball} ball the ball that being removed
         */
        blockedBallRemoved (ball : Ball) : void;

55 56 57 58 59
        /**
         * This will get invoked every time we're told to generate a maze and
         * our generation is finally completed.
         */
        mazeGenerationComplete () : void;
60 61 62 63 64 65 66 67 68 69

        /**
         * This will get invoked by the Maze update loop when it actively reaps
         * gray bricks that are finished vanishing (or just plain hidden) and it
         * detects that there are no further such bricks left.
         *
         * Once this triggers, it cannot trigger again unless something puts
         * more bricks into the maze.
         */
        grayBrickRemovalComplete () : void;
70 71
    }

Terence Martin's avatar
Terence Martin committed
72 73 74 75 76 77
    /**
     * The entity that represents the maze in the game. This is the entire play
     * area of the game.
     */
    export class Maze extends Entity
    {
78 79 80 81 82 83
        /**
         * The object that gets events when important things happen. This is
         * currently constrained to a single owning object for simplicity.
         */
        private _listener : MazeEventListener;

84
        /**
85
         * The object that we use to store our maze contents.
86
         */
87
        private _contents : MazeContents;
88

89 90 91 92 93
        /**
         * The object that we use to generate our random mazes.
         */
        private _generator : MazeGenerator;

Terence Martin's avatar
Terence Martin committed
94
        /**
95
         * The size (in pixels) of the cells that make up the maze grid.
Terence Martin's avatar
Terence Martin committed
96
         */
97
        private _cellSize : number;
Terence Martin's avatar
Terence Martin committed
98 99 100 101 102 103 104 105

        /**
         * Our singular Brick entity that represents the empty (background)
         * brick.
         */
        private _empty : Brick;

        /**
106
         * Our singular brick entity that represents the solid (wall) brick.
Terence Martin's avatar
Terence Martin committed
107 108
         */
        private _solid : Brick;
Terence Martin's avatar
Terence Martin committed
109

110 111 112 113 114 115
        /**
         * Our singular black hole entity that represents all black holes in the
         * maze.
         */
        private _blackHole : Teleport;

Terence Martin's avatar
Terence Martin committed
116 117 118 119 120 121
        /**
         * Our singular marker entity; this is used to render a marker at all
         * marked locations.
         */
        private _marker : Marker;

Terence Martin's avatar
Terence Martin committed
122
        /**
123 124 125
         * An actor pool which contains all of the arrow entities we've created
         * so far. The arrows that are in the live list are currently in the
         * level.
Terence Martin's avatar
Terence Martin committed
126
         */
127
        private _arrows : ActorPool<Arrow>;
Terence Martin's avatar
Terence Martin committed
128

129 130 131 132 133 134 135
        /**
         * An actor pool which contains all of the gray brick entities we've
         * created so far. The bricks that are in the live list are currently in
         * the level.
         */
        private _grayBricks : ActorPool<Brick>;

Terence Martin's avatar
Terence Martin committed
136 137 138 139 140 141 142
        /**
         * An actor pool which contains all of the bonus brick entities we've
         * created so far. The bricks that are in the live list are currently
         * in the level.
         */
        private _bonusBricks : ActorPool<Brick>;

143 144 145 146 147 148
        /**
         * An actor pool which contains all of the ball entities we've created.
         * This is always a set number.
         */
        private _balls : ActorPool<Ball>;

Terence Martin's avatar
Terence Martin committed
149 150 151 152
        /**
         * If a ball is actively dropping through the maze, this value will be
         * the ball entity that is dropping down. In this case the ball is not
         * currently considered a part of the maze (it is removed from the
153
         * grid entirely) and will be added back when it stops moving.
Terence Martin's avatar
Terence Martin committed
154 155 156 157 158
         *
         * When this value is null, no ball is currently dropping.
         */
        private _droppingBall : Ball;

159 160 161 162 163 164 165 166 167 168 169 170
        /**
         * This gets set to a valid ball at the same time as _droppingBall value
         * does during a ball drop. It retains it's value until the event
         * triggers that tells the caller that the ball has finished dropping.
         *
         * This is needed because we have to clear _droppingBall to let our
         * update code know to stop trying to drop it, but we need to keep it
         * around because the event that notifies of the ball stop may happen
         * several updates later if the ball is vanishing.
         */
        private _lastDroppedBall : Ball;

171 172 173
        /**
         * This flag is set to true when a ball has finished moving and is now
         * finalized. There are two conditions under which this is true; when
174 175
         * the ball stops somewhere in the maze, is told to vanish, and then has
         * finished vanishing.
176
         *
177 178 179
         * This is used to know when it is time to tell our listener that the
         * drop is complete, which can happen multiple ticks after the ball
         * comes to rest.
180 181 182
         */
        private _ballMoveFinalized : boolean;

183 184 185 186 187
        /**
         * This flag is set when we are dropping a ball through the maze as a
         * result of all moves having been made and all gray bricks being
         * removed.
         *
188 189
         * This is used to tell the drop code that when the ball comes to rest,
         * it should vanish away even if it didn't reach the goal.
190 191 192
         */
        private _droppingFinalBall : boolean;

Terence Martin's avatar
Terence Martin committed
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
        /**
         * The entire tick the last time the ball dropped down or otherwise moved.
         *
         * When this value plus _dropSpeed meets or exceeds the current engine
         * tick, the ball needs to take a new movement step.
         */
        private _lastDropTick : number;

        /**
         * The number of ticks between drop movements in the ball. Ticks are
         * counted in frames per second and the engine runs at 30fps (or tries
         * to), so a value of 30 means 1 second between steps.
         */
        private _dropSpeed : number;

208 209 210 211 212
        /**
         * The object that handles our debug options.
         */
        private _debugger : MazeDebugger;

213 214 215 216 217 218 219 220 221
        /**
         * A special marker instance that is used to show the current debug
         * point while debug tracking is turned on.
         *
         * This is like a regular marker but displays in an alternate color to
         * distinguish it.
         */
        private _debugMarker : Marker;

222 223 224 225 226 227 228 229
        /**
         * Get the size (in pixels) of the cells in the maze based on the
         * current sprite set. The cells are square, so this represents both
         * dimensions.
         *
         * @returns {number} the pixel size of the cells in the grid
         */
        get cellSize () : number
230
        { return this._cellSize; }
231

232 233 234 235 236 237 238 239 240
        /**
         * Get the object that stores the actual contents of this maze. Using
         * this object the state of the maze can be queried or updated.
         *
         * @returns {MazeContents} the object that holds our contents.
         */
        get contents () : MazeContents
        { return this._contents; }

241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263
        /**
         * Get the object that is currently being told about events happening in
         * this maze object; this can be null.
         *
         * @returns {MazeEventListener} the current event listener if any, or
         * null otherwise.
         */
        get listener () : MazeEventListener
        { return this._listener; }

        /**
         * Set the object that will be told about events happening in this maze
         * object. This will replace any existing listener.
         *
         * You can set this to null to turn off event listening for the
         * currently registered object.
         *
         * @param {MazeEventListener} newListener the object to use as listener
         * or null for none
         */
        set listener (newListener : MazeEventListener)
        { this._listener = newListener; }

264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283
        /**
         * Get the object that is used to generate the contents of this maze.
         * Using this object, external code can regenerate or otherwise tweak
         * the maze.
         *
         * @returns {MazeGenerator} the object that handles our generation.
         */
        get generator () : MazeGenerator
        { return this._generator; }

        /**
         * Get the object that is used for debugging. This contains methods that
         * can be used to turn debugging on and off and interact with the maze
         * in a variety of ways.
         *
         * @returns {MazeDebugger} the object that handles our debugging
         */
        get debugger () : MazeDebugger
        { return this._debugger; }

Terence Martin's avatar
Terence Martin committed
284 285 286 287 288 289 290 291 292 293 294 295
        /**
         * Construct a new empty maze entity.
         *
         * @param {Stage} stage the stage that we use to render ourselves
         */
        constructor (stage : Stage)
        {
            // Invoke the super; note that this does not set a position because
            // that is set by whoever created us. Our dimensions are based on
            // the size of the brick sprites, which we don't know yet.
            super ("maze", stage, 0, 0, 0, 0, 1, {}, {}, 'blue');

296 297 298
            // There is no listener by default.
            this._listener = null;

Terence Martin's avatar
Terence Martin committed
299 300 301 302 303 304
            // Set up a preload for the same sprite sheet that the brick entities
            // are using. This will allow us to capture the callback that
            // indicates that the sprite size is known, so that we can set up
            // our dimensions.
            new SpriteSheet (stage, "sprites_5_12.png", 5, 12, true, this.setDimensions);

305 306 307 308 309 310
            // Create our singleton maze entities; these are entities for which
            // we only ever have a single instance that's used everywhere.
            this._empty = new Brick (stage, BrickType.BRICK_BACKGROUND);
            this._solid = new Brick (stage, BrickType.BRICK_SOLID);
            this._blackHole = new Teleport (stage);

311 312 313 314
            // Create our maze contents, generator, and debugger; order is
            // important here, the generator and debugger need to get the
            // contents from us to initialize, and the debugger requires the
            // generator to already be available.
315
            this._contents = new MazeContents ();
316
            this._generator = new MazeGenerator (this);
317
            this._debugger = new MazeDebugger (this);
318
            this._generator.wall = this._solid;
319
            this._debugger.wall = this._solid;
320
            this._generator.teleporter = this._blackHole;
321
            this._debugger.teleporter = this._blackHole;
322

323
            // Create our entity pools.
324
            this._arrows = new ActorPool<Arrow> ();
325
            this._grayBricks = new ActorPool<Brick> ();
Terence Martin's avatar
Terence Martin committed
326
            this._bonusBricks = new ActorPool<Brick> ();
327
            this._balls = new ActorPool<Ball> ();
328

Terence Martin's avatar
Terence Martin committed
329 330 331 332
            // There is no ball dropping by default; also set up default values
            // for the drop time and speed (drop time is not consulted unless
            // a ball is dropping).
            this._droppingBall = null;
333
            this._lastDroppedBall = null;
334
            this._dropSpeed = NORMAL_DROP_SPEED;
Terence Martin's avatar
Terence Martin committed
335 336
            this._lastDropTick = 0;

337 338 339
            // No ball has finished moving and no gray bricks have been removed.
            // These also get reset on level generation.
            this._ballMoveFinalized = false;
340
            this._droppingFinalBall = false;
341

342
            // Pre-populate all of our actor pools with the maximum possible
343
            // number of actors that we could need.
344
            //
345 346 347
            // This is here to get around a ts-game-engine bug that stops creation
            // of entities that load images after the preload is finished.
            for (let i = 0 ; i < this._generator.maxArrows ; i++)
348
                this._arrows.addEntity (new Arrow (stage), false);
349
            for (let i = 0 ; i < this._generator.maxGrayBricks ; i++)
350
                this._grayBricks.addEntity (new Brick (stage, BrickType.BRICK_GRAY), false);
351
            for (let i = 0 ; i < this._generator.maxBonusBricks ; i++)
352
                this._bonusBricks.addEntity (new Brick (stage, BrickType.BRICK_BONUS), false);
Terence Martin's avatar
Terence Martin committed
353

354 355 356 357 358
            // Fill the actor pool for balls with a complete set of balls; this
            // only ever happens once and is the one case where we always know
            // exactly how many entities of a type we need.
            for (let i = 0 ; i < (MAZE_WIDTH - 2) * 2 ; i++)
                this._balls.addEntity (new Ball (stage), false);
Terence Martin's avatar
Terence Martin committed
359 360
        }

361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397
        /**
         * This callback is invoked when our sprite sheet finishes loading the
         * underlying image for the sprites.
         */
        private setDimensions = (sheet : SpriteSheet) : void =>
        {
            // Alter our collision properties so that our bounds represent the
            // entire maze area.
            this.makeRectangle (sheet.width * MAZE_WIDTH, sheet.height * MAZE_HEIGHT);

            // Set the cell size now.
            this._cellSize = sheet.width;

            // Determine how much width is left on the stage that is not taken
            // up by us.
            let remainder = this._stage.width - this.width;

            // Create a marker entity and set it's dimensions based on the
            // sprite sheet we loaded. Our callback might get invoked before
            // that of the _empty entity that our cellSize property returns,
            // so it's not safe to reference it here.
            this._marker = new Marker (this._stage, sheet.width);

            // Create the debug marker. This is as above, but we modify its
            // debug color to visually distinguish it. We need to violate the
            // privacy rules here because this is not supposed to be externally
            // touchable.
            this._debugMarker = new Marker (this._stage, sheet.width);
            this._debugMarker["_debugColor"] = 'red';

            // Set our position to center us on the screen horizontally and be
            // just slightly up from the bottom of the screen. We use half of
            // the remainder of the width, so that the bottom edge is as far
            // from the bottom of the screen as the side edges are.
            this.setStagePositionXY (Math.floor ((this._stage.width / 2) - (this.width  / 2)),
                                     Math.floor (this._stage.height - this.height - (remainder / 2)));

398 399 400 401
            // Now that we know our position and cell size, set that into the
            // maze contents so that it can update the position of things.
            this._contents.cellSize = this._cellSize;
            this._contents.position = this._position;
402 403
        }

404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422
        /**
         * Get an arrow from the arrow pool; may return null if none are
         * available.
         */
        getArrow () : Arrow { return this._arrows.resurrectEntity (); }

        /**
         * Get a gray brick from the arrow pool; may return null if none are
         * available.
         */
        getGrayBrick () : Brick { return this._grayBricks.resurrectEntity (); }

        /**
         * Get a bonus brick from the arrow pool; may return null if none are
         * available.
         */
        getBonusBrick () : Brick { return this._bonusBricks.resurrectEntity (); }

        /**
Terence Martin's avatar
Terence Martin committed
423
         * Get a ball from the ball pool; may return null if none are
424 425 426 427
         * available.
         */
        getBall () : Ball { return this._balls.resurrectEntity (); }

428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448
        /**
         * Take a point in stage coordinates and use it to set the current debug
         * location, if possible.
         *
         * If the point is within the bounds of this maze on the stage, it will
         * be used to change the current debug point. Otherwise, nothing
         * happens.
         *
         * This can be invoked even when the debug flag is turned off, although
         * in that case the set value is not used.
         *
         * @param {Point} position the position to track on the stage
         */
        setDebugPoint (position : Point) : void
        {
            // Use this point as long as it is contained inside of us.
            if (this.contains (position))
            {
                // Set our debug position the one provided, translate it to make
                // it local to our location on the stage, and then reduce it to
                // a cell coordinate.
449 450 451
                this._debugger.debugPoint.setTo (position);
                this._debugger.debugPoint.translateXY (-this._position.x, - this._position.y);
                this._debugger.debugPoint.reduce (this.cellSize);
452 453 454
            }
        }

Terence Martin's avatar
Terence Martin committed
455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476
        /**
         * Attempt to push the ball that exists in the top row of the given
         * column in the maze, if possible.
         *
         * The ball can only be pushed if the cell in the maze at that position
         * is not empty, is a ball, and there is not already a ball dropping.
         *
         * The return value tells you if the drop started or not.
         *
         * @param   {number}  column the column in the maze to push the ball in
         *
         * @returns {boolean}        true if the push worked and ball is
         * starting to drop, or false otherwise
         */
        pushBall (column : number) : boolean
        {
            // Try to get the entity in the first row of the given column. If
            // it exists and it is a ball, push it.
            let entity = this._contents.getCellAt (column, 0);
            if (entity != null && entity.name == "ball" && this._droppingBall == null)
            {
                // Drop it and leave.
477
                this.dropBall (<Ball> entity, NORMAL_DROP_SPEED, false);
Terence Martin's avatar
Terence Martin committed
478 479 480 481 482 483
                return true;
            }

            return false;
        }

484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503
        /**
         * DEBUG METHOD
         *
         * This takes a point that is representative of a mouse click inside of
         * the maze (i.e. the point (0, 0) is the upper left corner of this
         * entity) and "handles" it, using whatever debug logic we deem
         * exciting.
         *
         * This should return true or false depending on if it did anything with
         * the point or not, so the scene knows if the default handling should
         * be applied or not.
         *
         * @param   {Point}   position the position in our bounds of the click
         *
         * @returns {boolean}          true if we handled the click, or false
         * otherwise
         */
        handleClick (position : Point) : boolean
        {
            // The position is in pixels, so reduce it down to the size of the
504 505
            // cells in the maze, then collect the entity out of the maze at
            // that location (if any).
506
            position.reduce (this.cellSize);
507
            let entity = this._contents.getCellAt (position.x, position.y);
508

509 510 511 512 513 514 515 516 517
            // If this cell in the maze does not contain anything, or it
            // contains the black hole, then toggle the marker at this location.
            if (entity == null || entity == this._blackHole)
            {
                // Toggle the marker here.
                this._contents.toggleMarkerAt (position.x, position.y);
                return;
            }

Terence Martin's avatar
Terence Martin committed
518 519
            // If the entity is a ball and we're not already trying to drop a
            // ball, try to move it downwards.
520
            if (entity.name == "ball" && this._droppingBall == null)
Terence Martin's avatar
Terence Martin committed
521
            {
522
                // Drop it and leave.
523
                this.dropBall (<Ball> entity, NORMAL_DROP_SPEED, false);
Terence Martin's avatar
Terence Martin committed
524 525 526 527 528
                return true;
            }

            // If we're not tracking debug action, the rest of these actions
            // should not be allowed;
529
            if (this._debugger.debugTracking == false)
Terence Martin's avatar
Terence Martin committed
530 531
                return;

532 533
            // If this is a brick that is not hidden, vanish it. We can't bring
            // it back because once it's hidden the update loop will reap it.
534
            if (entity.name == "brick")
535
            {
Terence Martin's avatar
Terence Martin committed
536 537
                // Clear any marker that might be here; these can only appear if
                // the ball drops through, so lets be able to remove them.
538
                this._contents.clearMarkerAt (position.x, position.y);
Terence Martin's avatar
Terence Martin committed
539

540
                // Get the brick; if its not hidden, vanish it.
541
                let brick = <Brick> entity;
542
                if (brick.isHidden == false)
543
                    brick.vanish ();
544 545 546 547
            }

            // If it is an arrow, flip it. This works for any type of arrow; an
            // automatic arrow will reset its random flip time in this case.
548
            if (entity.name == "arrow")
549 550
            {
                let arrow = <Arrow> entity;
551
                arrow.flip (false);
552 553 554 555 556
                return true;
            }

            // We care not for this click.
            return false;
557 558
        }

559 560 561 562 563 564 565 566
        /**
         * Given a ball entity which exists in the maze, set up to start
         * dropping it through the maze, setting everything up as needed.
         *
         * For this to work, the ball provided must be stored in the maze and
         * its map position must accurately reflect the position it is stored
         * in, since that position will be cleared when the ball starts moving.
         *
567 568 569 570 571 572 573
         * When isFinal is true, this ball will be vanished as soon as it stops
         * moving, even if it doesn't reach the goal.
         *
         * @param {Ball}    ball    the ball to drop
         * @param {number}  speed   the number of ticks between ball step stages
         * @param {boolean} isFinal true if this is a final ball drop or false
         * otherwise
574
         */
575
        private dropBall (ball : Ball, speed : number, isFinal: boolean) : void
576
        {
577 578
            // Set the flag that indicates if this drop is a final drop or not.
            this._droppingFinalBall = isFinal;
579

580 581 582 583 584 585
            // Get the maze contents to mark this ball as played. If this is
            // one of the generated human or computer balls from the top row,
            // this will remove it from the list of balls so that the code knows
            // that this ball is no longer available.
            this._contents.markBallPlayed (ball);

586 587 588 589
            // Set the entity that is currently dropping to the one provided,
            // then remove it from the maze. It will be re-added when
            // it is finished moving
            this._droppingBall = ball;
590
            this._lastDroppedBall = ball;
591 592 593 594 595 596 597 598 599 600 601 602
            this._contents.clearCellAt (ball.mapPosition.x, ball.mapPosition.y);

            // Ensure that the ball knows before we start that it started
            // out not moving.
            this._droppingBall.moveType = BallMoveType.BALL_MOVE_NONE;

            // Now indicate that the last time the ball dropped was right now
            // so that the next step in the drop happens in the future.
            this._lastDropTick = this._stage.tick;

            // Set up the drop speed.
            this._dropSpeed = speed;
Terence Martin's avatar
Terence Martin committed
603 604 605 606 607

            // Now that the ball has started moving, if there is a listener,
            // tell it.
            if (this._listener != null)
                this._listener.startBallPush (ball);
608 609
        }

610 611
        /**
         * Given a point that represents the position that is expected to be a
612 613 614 615 616 617 618
         * ball, calculate where the next position that it should be is.
         *
         * The possible position changes are:
         *    1) the cell below us allows the ball to enter it or is empty, so
         *       drop down one.
         *    2) The cell below us is an arrow which shoves us one space to the
         *       left or right, possibly.
619 620
         *    3) The cell below us is a teleport; the ball position potentially
         *       jumps elsewhere.
621 622 623 624 625
         *
         * If the ball would stop at this location, false is returned back to
         * indicate this. Otherwise, the position passed in is modified to show
         * where the move would go next and true is returned.
         *
626 627 628 629 630 631 632 633
         * The isSimulation parameter indicates if this movement operation is
         * part of a simulation (e.g. for AI purposes) and is passed to the
         * appropriate event handlers on entities.
         *
         * When we're simulating the collisions still logically work but the
         * state of the objects is not permanently changed, so that we can
         * revert back to where we started without visual glitches.
         *
634 635
         * @param   {Ball}    ball     the ball that is moving
         * @param   {Point}   position the current position of the ball given
636 637
         * @param   {boolean} isSimulation true if this is part of a
         * simulation,
638
         *
639 640 641
         * @returns {boolean} true if the ball moved, false otherwise. When
         * true is returned, the passed in point is modified to show where the
         * new location is.
642
         */
Terence Martin's avatar
Terence Martin committed
643 644
        nextBallPosition (ball : Ball, position : Point,
                          isSimulation : boolean) : boolean
645 646 647 648
        {
            // If this position is in the second to last row of the maze, it has
            // reached the goal line, so movement stops.
            if (position.y == MAZE_HEIGHT - 2)
649 650
            {
                ball.moveType = BallMoveType.BALL_MOVE_NONE;
651
                return false;
652
            }
653

654
            // Get the contents of the cell where the ball is currently at, if
655 656 657
            // any; if there is one, tell it that the ball touched it, and also
            // possibly allow it to move the ball, as long as that's not how we
            // got at the current position.
658
            let current = this._contents.getCellAt (position.x, position.y);
659 660 661 662
            if (current != null)
            {
                // Copy the position provided and then hand it to the entity
                // that we're currently on top of.
663
                let newPos = current.ballTouch (this, ball, position, isSimulation);
664

665 666 667
                // If we're allowed to move the ball because of a touch and the
                // entity below us actually changed the location, then that is
                // the move for this cycle.
668
                if (ball.moveType != BallMoveType.BALL_MOVE_JUMP && newPos != null)
669
                {
670 671 672 673
                    // The movement type of a touch is a jump; the entity itself
                    // can't stamp this in because we never tell it if it
                    // successfully moved the ball or not.
                    ball.moveType = BallMoveType.BALL_MOVE_JUMP;
674 675

                    // Set the position to the one the entity provided.
676 677 678 679 680
                    position.setTo (newPos);
                    return true;
                }
            }

Terence Martin's avatar
Terence Martin committed
681 682
            // If the cell below us is not blocking the ball, we can drop the
            // ball into it and we're done.
Terence Martin's avatar
Terence Martin committed
683
            let below = this._contents.getBlockingCellAt (position.x, position.y + 1, isSimulation);
Terence Martin's avatar
Terence Martin committed
684
            if (below == null)
685
            {
686
                ball.moveType = BallMoveType.BALL_MOVE_DROP;
687
                position.y++;
688 689 690
                return true;
            }

691 692 693 694 695
            // The cell below has blocked our movement. Invoke the collision
            // routine with it. If this returns null, we're blocked and cannot
            // move, so return now.
            let newPos = below.ballCollision (this, ball, position);
            if (newPos == null)
696 697
            {
                ball.moveType = BallMoveType.BALL_MOVE_NONE;
698
                return false;
699
            }
700

701
            // Check the contents of the new location and see if the ball is
702 703
            // allowed to enter that cell or not; the ball can enter if the cell
            // is empty or does not block ball movement.
Terence Martin's avatar
Terence Martin committed
704
            if (this._contents.getBlockingCellAt (newPos.x, newPos.y, isSimulation) == null)
705
            {
706 707
                // Tell the cell that moved the ball that we actually moved it,
                // and then return back the position that it gave.
708 709 710
                //
                // In this case, it is up to the entity that moved the ball to
                // mark how it moved it, as we can't know.
711
                below.didMoveBall (ball, isSimulation);
712
                position.setTo (newPos);
713
                return true;
714 715
            }

716 717
            // The cell below us wants to shift our location to somewhere that
            // we're not allowed to enter, so just leave.
718
            ball.moveType = BallMoveType.BALL_MOVE_NONE;
719 720 721
            return false;
        }

722 723 724 725 726 727 728
        /**
         * Given an ActorPool that contains maze cells that conform to the
         * hide-able maze cell interface, scan the pool for all live entities
         * that are currently hidden and not actively hiding themselves and
         * remove them from the maze grid, killing the entity in the process.
         *
         * @param {ActorPool<HideableMazeCell>} pool the pool to reap
729 730 731
         *
         * @returns {number} the number of entities that were reaped during the
         * call, which may be 0.
732
         */
733
        private reapHiddenEntitiesFromPool (pool : ActorPool<HideableMazeCell>) : number
734
        {
735 736
            let retVal = 0;

737 738 739 740 741 742 743 744
            // Scan all of the live entities in the pool.
            for (let i = 0 ; i < pool.liveEntities.length ; i++)
            {
                // If this ball thinks it's hidden and it's animation is no
                // longer playing, we can remove it from the grid now.
                let cell = pool.liveEntities[i];
                if (cell.isHidden && cell.animations.isPlaying == false)
                {
745
                    this._contents.clearCellAt (cell.mapPosition.x, cell.mapPosition.y);
746
                    pool.killEntity (cell);
747
                    retVal++;
748 749
                }
            }
750 751

            return retVal;
752 753
        }

754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770
        /**
         * Scan the maze to find all entities that are ball entities that are
         * currently marked as hidden, and remove them from the maze by setting
         * that position in the maze to null.
         *
         * The return value indicates how many such balls were removed from the
         * maze during this call.
         *
         * @returns {number} the number of removed balls during this call, which
         * may be 0.
         */
        private clearHiddenBalls () : number
        {
            let retVal = 0;

            for (let row = 0 ; row < MAZE_HEIGHT - 1 ; row++)
            {
Terence Martin's avatar
Terence Martin committed
771
                for (let col = 1 ; col < MAZE_WIDTH - 1 ; col++)
772 773
                {
                    let ball = <Ball> this._contents.getCellAt (col, row);
774 775
                    if (ball != null && ball.name == "ball" &&
                        ball.isHidden && ball.animations.isPlaying == false)
776 777 778 779 780 781 782 783 784 785
                    {
                        this._contents.clearCellAt (col, row);
                        retVal++;
                    }
                }
            }

            return retVal;
        }

786 787 788 789 790 791 792 793 794 795 796
        /**
         * Select the next ball in the maze that should start it's final descent
         * through the maze.
         *
         * The return value indicates if a ball was found or not, so that the
         * caller knows if there is anything left to push.
         *
         * @returns {boolean} true if a ball was started dropping, or false
         * otherwise
         */
        dropNextFinalBall () : boolean
797 798
        {

799
            // Find a ball from the maze and drop it.
800 801 802 803
            for (let row = MAZE_HEIGHT - 2 ; row >= 0 ; row--)
            {
                for (let col = MAZE_WIDTH - 1 ; col >= 1 ; col--)
                {
804
                    let cell = this._contents.getCellAt (col, row);
805
                    if (cell != null && cell.name == "ball")
806
                    {
807
                        // Start this ball dropping.
808
                        this.dropBall (<Ball> cell, FINAL_DROP_SPEED, true);
809
                        return true;
810 811 812
                    }
                }
            }
813 814 815

            // There was nothing to push
            return false;
816 817
        }

818 819
        /**
         * Scan through the maze (left to right, bottom to top) looking for the
820 821 822 823 824
         * first gray brick entity that has not already been told to vanish and
         * tell it to.
         *
         * If there are no gray bricks at all in the maze, this will return
         * false to indicate that there can be no brick removal.
825
         *
826 827 828 829 830 831 832 833 834 835 836 837
         * The return value will be true if a brick was told to vanish OR we ran
         * across a brick that is hidden but still in the maze; in this case we
         * know that it has not vanished yet, so we can wait.
         *
         * This allows calling code to detect when there are no gray bricks at
         * all (debugging) so that it can skip over the state where we remove
         * gray bricks, but still make sure that we can naturally trigger the
         * "all gray bricks are now vanished" event once the last of them
         * vanishes away and is reaped.
         *
         * @returns {boolean} false if there are no gray bricks in the maze at
         * all or true otherwise
838
         */
839
        removeNextGrayBrick () : boolean
840
        {
841 842 843 844
            // Assume be default we did not see any gray bricks at all.
            let sawBrick = false;

            // Scan from the bottom up.
845 846 847 848 849 850 851
            for (let row = MAZE_HEIGHT - 2 ; row >= 0 ; row--)
            {
                for (let col = 1 ; col < MAZE_WIDTH - 1 ; col++)
                {
                    // Get the cell as a brick (it may not be).
                    let cell = <Brick> this._contents.getCellAt (col, row);

852 853
                    // If we got a cell and it's a gray brick, it might be
                    // interesting.
854
                    if (cell != null && cell.name == "brick" &&
855
                        cell.brickType == BrickType.BRICK_GRAY)
856
                    {
857 858 859 860 861 862 863 864 865 866 867
                        // If the brick is not already hidden, hide it and return
                        // true right away.
                        if (cell.isHidden == false)
                        {
                            cell.vanish ();
                            return true;
                        }

                        // It's already hidden so we need to ignore it, but at
                        // least we saw it.
                        sawBrick = true;
868 869 870
                    }
                }
            }
871 872

            return sawBrick;
873 874
        }

875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928
        /**
         * Scan through the maze from the bottom to the top looking for balls
         * that we need to vanish away because there is no possibility of them
         * moving further.
         *
         * Primarily this is a ball that is either directly resting on an arrow
         * or a ball that is resting on a ball that is an arrow.
         *
         * In practice since we are scanning from the bottom up, we remove a
         * ball in the situations mentioned above as well as when the cell below
         * a ball is either empty or a hidden ball, both of which being a
         * consequence of a previous call to this method having been invoked.
         *
         * The return value is false in the exact situation where there are no
         * balls at all in the maze that require being removed or true if we
         * told a ball to vanish or there is at least still one ball waiting to
         * finish vanishing.
         *
         * This should be called repeatedly (over time) until it returns false.
         *
         * @returns {boolean} true if there are still balls to remove/waiting to
         * be removed or false when no balls need to be removed any longer.
         */
        removeNextBlockedBall () : boolean
        {
            let sawBall = false;

            // Scan from the bottom up.
            for (let row = MAZE_HEIGHT - 2 ; row >= 0 ; row--)
            {
                for (let col = 1 ; col < MAZE_WIDTH - 1 ; col++)
                {
                    // Get the cell that we're searching for as a ball and the
                    // cell below it.
                    let cell = <Ball> this._contents.getCellAt (col, row);
                    let below = this._contents.getCellAt (col, row + 1);

                    // Skip this cell if it is empty or not a ball.
                    if (cell == null || cell.name != "ball")
                        continue;

                    // This is a ball; if it has already been hidden we can skip
                    // any further checks, but set our flag to indicate that we're
                    // still waiting for this ball to be removed.
                    if (cell.isHidden == true)
                    {
                        sawBall = true;
                        continue;
                    }

                    // This is a ball that is not already hidden. The criteria
                    // for hiding a ball during this call are that it is not
                    // already hidden (not already selected) and:
                    //
929
                    //   1) The cell below is a blank space (has to be a ball
930
                    //      that we previously vanished with a call like this)
931 932 933 934 935
                    //   2) What is below is a ball that is hidden (will soon
                    //      become #1, just not there yet)
                    //   3) The cell below is an arrow
                    if (below == null ||
                        below.name == "arrow" ||
936 937
                        (below.name == "ball" && (<Ball>below).isHidden == true))
                    {
938 939 940
                        // Tell our listener (if any) that this is happening
                        if (this._listener != null)
                            this._listener.blockedBallRemoved (cell);
941 942 943 944 945 946 947 948 949 950
                        cell.vanish ();
                        return true;
                    }
                }
            }

            // Return if we saw a ball waiting to vanish or not.
            return sawBall;
        }

951 952 953 954 955 956 957 958 959 960 961 962 963 964
        /**
         * This is called every frame update (tick tells us how many times this
         * has happened) to allow us to update ourselves.
         *
         * This invokes the superclass method, and then makes sure to also
         * invoke the update method for our animated MazeCell entities, so that
         * their animations will play as expected.
         *
         * @param {Stage}  stage the stage that we are on
         * @param {number} tick  the current engine tick; this advances once for
         * each frame update
         */
        update (stage : Stage, tick : number) : void
        {
Terence Martin's avatar
Terence Martin committed
965
            // Let the super do it's thing for us.
966
            super.update (stage, tick);
967

Terence Martin's avatar
Terence Martin committed
968
            // Make sure the black holes animate.
969
            this._blackHole.update (stage, tick);
970 971 972

            // Now update all of the entities in our various entity pools.
            this._arrows.update (stage, tick);
973
            this._grayBricks.update (stage, tick);
Terence Martin's avatar
Terence Martin committed
974
            this._bonusBricks.update (stage, tick);
975
            this._balls.update (stage, tick);
Terence Martin's avatar
Terence Martin committed
976

977 978
            // Reap any dead balls; these are balls which are currently
            // invisible but still alive; they can be removed from the grid now.
979
            //
980 981 982 983 984 985 986
            // When this happens and we are dropping a ball, then we can set the
            // flag that indicates that the ball movement is finalized, so that
            // we can tell our listener that the move is done now.
            //
            // This also gets triggered during non-dropped ball removal, such
            // as vanishing blocked balls, but in that case we don't set the
            // flag because the appropriate handling has already been done.
987
            if (this.clearHiddenBalls () > 0 && this._lastDroppedBall != null)
988 989 990 991 992 993
                this._ballMoveFinalized = true;

            // Reap any dead gray bricks; these are the gray bricks that have
            // been vanished out of the level because all of the balls have been
            // played.
            //
994 995
            // If this collects all gray bricks, we can tell our listener that
            // we're done removing them now.
996 997
            if (this.reapHiddenEntitiesFromPool (this._grayBricks) > 0 &&
                    this._grayBricks.liveEntities.length == 0)
998 999 1000 1001
            {
                if (this._listener != null)
                    this._listener.grayBrickRemovalComplete ();
            }
1002

Terence Martin's avatar
Terence Martin committed
1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017
            // If there is a dropping ball and it's time to drop it, take a step
            // now.
            if (this._droppingBall && tick >= this._lastDropTick + this._dropSpeed)
            {
                // We are going to drop the ball (or try to), so reset the last
                // drop tick to this tick.
                this._lastDropTick = tick;

                // Get the current position of the ball; this is just an alias
                // to the actual object.
                let pos = this._droppingBall.mapPosition;

                // Check to see what the next position of the ball is. If this
                // returns false, the ball is not going to move, so we are done
                // moving it now.
1018
                if (this.nextBallPosition (this._droppingBall, pos, false) == false)
Terence Martin's avatar
Terence Martin committed
1019 1020
                {
                    // Add the ball back to the maze at it's current position.
1021
                    this._contents.setCellAt (pos.x, pos.y, this._droppingBall);
Terence Martin's avatar
Terence Martin committed
1022

1023 1024 1025 1026 1027
                    // If the ball position is at the bottom of the maze or it
                    // is one of the final balls, then, get it to play it's
                    // vanish animation. When this is not the case, the ball
                    // stopped somewhere in the maze. In this case we set the
                    // flag that says the ball is done moving right away.
1028 1029 1030 1031 1032
                    //
                    // A ball that is vanishing sets this flag when it gets
                    // reaped, so that the code that triggers when the flag
                    // becomes set to true doesn't happen until the ball is
                    // visibly gone.
1033
                    if (pos.y == MAZE_HEIGHT - 2 || this._droppingFinalBall == true)
Terence Martin's avatar
Terence Martin committed
1034
                        this._droppingBall.vanish ();
1035 1036
                    else
                        this._ballMoveFinalized = true;
Terence Martin's avatar
Terence Martin committed
1037 1038 1039 1040

                    // Now clear the flag so we know we're done.
                    this._droppingBall = null;
                }
1041 1042 1043 1044 1045 1046 1047 1048
                else
                {
                    // The ball moved, so update it's location on the screen
                    // as well.
                    this._droppingBall.position.setTo (pos);
                    this._droppingBall.position.scale (this.cellSize);
                    this._droppingBall.position.translate (this._position);
                }
Terence Martin's avatar
Terence Martin committed
1049
            }
1050 1051 1052 1053 1054 1055 1056

            // When this flag is set, it means that a ball has been dropped and
            // is now finished moving. This can either have triggered from the
            // code above, or if the code above vanished the ball, the code that
            // reaps the dead ball when it is finished vanishing sets this flag
            // for us.
            //
1057 1058 1059 1060 1061
            // In either case, this is our chance to tell any listener that the
            // drop is fully complete.
            //
            // NOTE: If the ball reached the goal, it was vanished and is now
            // dead. However its map position remains the same.
1062 1063 1064 1065 1066
            if (this._ballMoveFinalized)
            {
                // Reset the flag now for next time
                this._ballMoveFinalized = false;

1067 1068
                // If there is a listener, tell it that this ball has stopped
                // moving now.
1069
                if (this._listener != null)
1070
                    this._listener.ballDropComplete (this._lastDroppedBall);
1071

1072 1073 1074
                // Done with the value now.
                this._lastDroppedBall = null;
            }
1075 1076
        }

1077 1078 1079 1080 1081 1082 1083
        /**
         * This will render the backing portion of the maze, which will draw in
         * the bounding walls on the outer edges as well as a complete grid of
         * background tiles.
         *
         * This effectively draws what looks like a completely empty grid.
         *
1084 1085 1086
         * @param {number}   x        the X coordinate to start drawing at
         * @param {number}   y        the y coordinate to start drawing at
         * @param {number}   cSize    the size of the grid cells, in pixels
1087 1088
         * @param {Renderer} renderer the render to use during rendering
         */
1089
        private renderMazeBacking (x : number, y : number, cSize : number, renderer : Renderer) : void
1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109
        {
            // Iterate over all of the cells that make up the maze, rendering
            // as appropriate.
            for (let cellY = 0, blitY = y ; cellY < MAZE_HEIGHT ; cellY++, blitY += cSize)
            {
                for (let cellX = 0, blitX = x ; cellX < MAZE_WIDTH ; cellX++, blitX += cSize)
                {
                    // The cell to render is empty, unless this is the side of
                    // the maze or the bottom of it, in which case the wall is
                    // solid.
                    let cell = this._empty;
                    if (cellX == 0 || cellX == MAZE_WIDTH - 1 || cellY == MAZE_HEIGHT - 1)
                        cell = this._solid;

                    // Render this cell.
                    cell.render (blitX, blitY, renderer);
                }
            }
        }

1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127
        /**
         * Render the markers in the maze; these are set manually by the user
         * clicking on the grid while in debug mode.
         *
         * @param {number}   x        the X coordinate to start drawing at
         * @param {number}   y        the y coordinate to start drawing at
         * @param {number}   cSize    the size of the grid cells, in pixels
         * @param {Renderer} renderer the renderer to use to render the markers.
         */
        private renderMazeMarkers (x : number, y : number, cSize : number, renderer : Renderer) : void
        {
            // Iterate over all columns and rows and render any markers that
            // might exist.
            for (let cellY = 0, blitY = y ; cellY < MAZE_HEIGHT ; cellY++, blitY += cSize)
            {
                for (let cellX = 0, blitX = x ; cellX < MAZE_WIDTH ; cellX++, blitX += cSize)
                {
                    // If this position contains a marker, render one here.
1128
                    if (this._contents.hasMarkerAt (cellX, cellY))
1129 1130 1131 1132 1133
                        this._marker.render (blitX, blitY, renderer);
                }
            }
        }

Terence Martin's avatar
Terence Martin committed
1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144
        /**
         * Render us onto the stage provided at the given position.
         *
         * This renders us by displaying all entities stored in the maze.
         *
         * @param {number}   x        the X coordinate to start drawing at
         * @param {number}   y        the y coordinate to start drawing at
         * @param {Renderer} renderer the renderer to use to render
         */
        render (x : number, y : number, renderer : Renderer) : void
        {
Terence Martin's avatar
Terence Martin committed
1145 1146 1147
            // Get the cell size of our cells so we know how to blit.
            let cSize = this.cellSize;

1148 1149
            // Render the background of the maze first. This will draw the
            // background and the walls along the sides.
1150
            this.renderMazeBacking (x, y, cSize, renderer);
Terence Martin's avatar
Terence Martin committed
1151

1152 1153 1154
            // Render all of the black holes; for this we have to iterate the
            // list of known destinations and use them to calculate the
            // appropriate position.
1155 1156 1157
            //
            // Black holes have to come first so that if the ball comes to
            // rest on top of them, we can still see it.
1158 1159 1160 1161 1162 1163 1164 1165
            for (let i = 0 ; i < this._blackHole.length ; i++)
            {
                let pos = this._blackHole.destinationList[i];
                this._blackHole.render (x + (pos.x * cSize),
                                        y + (pos.y * cSize),
                                        renderer);
            }

1166 1167 1168 1169
            // Now render everything else.
            this._arrows.render (renderer);
            this._grayBricks.render (renderer);
            this._bonusBricks.render (renderer);
Terence Martin's avatar
Terence Martin committed
1170
            this._balls.render (renderer);
1171

1172 1173 1174
            // We can render the markers now.
            this.renderMazeMarkers (x, y, cSize, renderer);

1175
            // Now the debug marker, if it's turned on.
1176
            if (this._debugger.debugTracking)
Terence Martin's avatar
Terence Martin committed
1177
            {
1178
                let pos = this._debugger.debugPoint;
Terence Martin's avatar
Terence Martin committed
1179 1180
                this._debugMarker.render (x + (pos.x * cSize),
                                          y + (pos.y * cSize),
1181
                                          renderer);
Terence Martin's avatar
Terence Martin committed
1182
            }
Terence Martin's avatar
Terence Martin committed
1183 1184 1185
        }

        /**
1186
         * Reset all of the maze entities.
1187
         *
1188 1189 1190
         * This will kill all of the living entities, clear all markers, and get
         * all entities used in the maze back into their clean starting state
         * for a new maze generation sequence.
1191
         *
1192 1193 1194
         * This does not modify the contents of the maze, so things are likely
         * to break if you don't clear it yourself or generate a maze right
         * away.
1195
         */
1196
        resetMazeEntities () : void
1197
        {
1198 1199 1200 1201 1202
            // Make sure that all of the entity pools are emptied out by killing
            // everything in them.
            this._arrows.killALl ();
            this._grayBricks.killALl ();
            this._bonusBricks.killALl ();
1203
            this._balls.killALl ();
1204
            this._contents.clearMarkers ();
1205

Terence Martin's avatar
Terence Martin committed
1206 1207 1208
            // Make sure that our black hole entity doesn't know about any
            // destinations from a prior maze (if any).
            this._blackHole.clearDestinations ();
1209 1210 1211 1212 1213 1214
        }

        /**
         * Generate a new maze; this sets everything up for a new round of the
         * game.
         */
1215
        generateMaze () : void
1216
        {
1217 1218
            // Kill all living entities to get everything into a clean state.
            this.resetMazeEntities ();
1219

1220 1221
            // No ball has finished moving and no gray bricks have been removed.
            this._ballMoveFinalized = false;
1222
            this._droppingFinalBall = false;
1223

1224
            // Now generate the contents of the maze.
1225
            this._generator.generate ();
1226 1227 1228 1229 1230

            // If there is a listener, tell it now that the generation has
            // completed.
            if (this._listener != null)
                this._listener.mazeGenerationComplete ();
1231
        }
Terence Martin's avatar
Terence Martin committed
1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276

        /**
         * Inform the maze that one or more simulations are about to commence.
         * This will make sure to tell all entities for which it matters that
         * they should save their state.
         */
        beginSimulation () : void
        {
            // Save state in balls.
            for (let i = 0 ; i < this._balls.liveEntities.length ; i++)
                this._balls.liveEntities[i].enteringSimulation ();

            // Save state in bonus bricks
            for (let i = 0 ; i < this._bonusBricks.liveEntities.length ; i++)
                this._bonusBricks.liveEntities[i].enteringSimulation ();

            // Save state in arrows
            for (let i = 0 ; i < this._arrows.liveEntities.length ; i++)
                this._arrows.liveEntities[i].enteringSimulation ();

            // Nothing else needs to save state because it does not change per-
            // move.
        }

        /**
         * Inform the maze that a simulation cycle has now finished and everything
         * should be restored to its pre-simulation state.
         */
        endSimulation () : void
        {
            // Restore state in balls.
            for (let i = 0 ; i < this._balls.liveEntities.length ; i++)
                this._balls.liveEntities[i].exitingSimulation ();

            // Restore state in bonus bricks
            for (let i = 0 ; i < this._bonusBricks.liveEntities.length ; i++)
                this._bonusBricks.liveEntities[i].exitingSimulation ();

            // Restore state in arrows
            for (let i = 0 ; i < this._arrows.liveEntities.length ; i++)
                this._arrows.liveEntities[i].exitingSimulation ();

            // Nothing else needs to restore state because it does not change
            // per- move.
        }
Terence Martin's avatar
Terence Martin committed
1277 1278
    }
}