Monday, January 23, 2017

Phaser tutorial: How to balance random booleans for game

 





Previous Phaser tutorials and articles:
Phaser tutorial: Fun with bitmap fonts
Phaser tutorial: Using Spriter player for Phaser
Phaser tutorial: Merging fonts into sprite atlas
Phaser: Typescript defs for Phaser Box2D plugin
Phaser tutorial: Spriter Pro features added to Spriter player for Phaser
Phaser tutorial: Using Phaser signals
Phaser tutorial: Breaking the (z-order) law!
Phaser tutorial: Phaser and Spriter skeletal animation
Phaser tutorial: DronShooter - simple game in Typescript - Part 3
Phaser tutorial: DronShooter - simple game in Typescript - Part 2
Phaser tutorial: adding 9-patch image support to Phaser
Phaser tutorial: DronShooter - simple game in Typescript - Part 1
Phaser tutorial: custom easing functions for tweening and easing functions with parameters
Phaser tutorial: sprites and custom properties for atlas frames
Phaser tutorial: manage different screen sizes
Phaser tutorial: How to wrap bitmap text


Introduction


 Last year I worked on many small games, were obstacles were generated procedurally on the fly. Getting random numbers from random numbers generator played big role in it. Unfortunately, randomness can be sometimes pretty ... random. Let's say you just want to get randomly true or false to place some obstacle to the right or left. Be sure, that very quickly you will encounter situations, when you will not like the result. Sometimes, there will be too many obstacles on one or other side and game may get boring for long time periods.
 Fast fix may be to add some counters to check if not too many obstacles is on one or other side.
 I was solving this often and to make situation worse, clients had requests like: "I want first 5 obstacles to be left and right in turns, so it will be kind of tutorial for player."


Solution


 So, I decided to solve it once and forever with small class, which I named RandomBooleanBalancer. Here is complete code for it in TypeScript:

namespace Helper {

    export class RandomBooleanBalancer {

        private _rnd: Phaser.RandomDataGenerator;
        private _lbound: number;
        private _ubound: number;
        private _currentBalance: number;

        // -------------------------------------------------------------------------
        // balancer - how many consecutive same values maximally
        public constructor(rnd: Phaser.RandomDataGenerator, balancer: number) {
            this._rnd = rnd;
            this.reset(balancer);
        }

        // -------------------------------------------------------------------------
        public reset(balancer: number): void {
            --balancer;

            let lbound = Math.ceil(balancer / 2);
            this._lbound = -lbound;
            this._ubound = balancer - lbound;

            this._currentBalance = balancer === 0 ? this._rnd.integerInRange(-1, 0) : 0;
        }

        // -------------------------------------------------------------------------
        public getBoolean(): boolean {

            let result = this._currentBalance + this._rnd.integerInRange(this._lbound, this._ubound);

            //console.log("lbound = " + (this._lbound + this._currentBalance) + ", ubound = " + (this._ubound + this._currentBalance) +
            //    ", currentBalance = " + this._currentBalance + ", value = " + (result >= 0));

            this._currentBalance += result >= 0 ? -1 : 1;

            return result >= 0;
        }
    }
}

 In constructor it takes random numbers generator and "magic number" called balancer. This number is your request, how many consecutive same values it shall return when you repeatedly call getBoolean() method.

 For example, if you want true or false maximally two times after each other, then set it to 2.

 reset() method allows you to change this request during runtime.


Test


 Let's do some tests. Our test code will be simple loop with 50 iterations - it just prints string with 50 comma separated results, where true / false is converted to 1 / 0 to shorten output. We will start with balancer value equal 1:

            let randomBalancer = new Helper.RandomBooleanBalancer(this.rnd, 1);
            let result = "";
            for (let i = 0; i < 50; i++) {
                result += (randomBalancer.getBoolean() ? "1" : "0") + (i < 50 - 1 ? ", " : "");
            }
            console.log(result);

 Here is output from console:

0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1

 As our request was to have maximally 1 consecutive true or false, we got them switching regularly. If you run this test multiple times, you will notice, that whole sequence starts randomly with true or false.

 Now, change number in constructor to 2 and run the test again. This is console output:

0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1

 As you can see, returned values are random, but you get maximally two randoms of the same value after each other.

 Now, change requested value in constructor to 5. You may get something like this:

1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0

 In my run I did not get any sequence of 5 ones or zeroes. This is another nice feature of this class. As you get more and more, let's say, ones, there is decreasing probability, that next random will be also one. It is more and more probable it will be zero. But of course, it is possible to get true or false 5 times in row - but this is cap, as you requested: "5 times true or false in row, but no more".


Conclusion


 I hope this small code snippet will make your life easier. If you are interested in how it works, you can uncomment debug output in getBoolean() method and watch it after each call.








No comments:

Post a Comment