7
\$\begingroup\$

A long long time ago I asked this question which has since garnered about one thousand views. Now for something new.

The demo for what I have is located here. I am generally happy with what I have built (although I did not implement all of the visuals (yet) of the original).

I want to know if there is some improvements to my hitting algorithm that can be made, and maybe some suggestions of how to implement other fielders (aside from the pitcher that is already written).

const logger = ((logArea, logDelay = 0) => {
    const playArea = document.getElementById(logArea);
    return {
        delay: logDelay,
        log(...args) {
            let div = document.createElement('div');

      // Can see the result in the console
      console.log(...args);

            setTimeout(() => {
                // append to the top of the log
                playArea.insertBefore(div, playArea.firstChild);
                div.textContent = args.join(', ')
            }, this.delay);

            this.delay += logDelay;
        },
        logPad(...args) {
            this.log();
            this.log(...args);
        }
    };
})('playArea', 120);

const Team = (() => {
    const numberOfPlayers = 9;

    return class Team {
        constructor(name) {
            this.name = name;
            this.players = [];
            this.listeners = [];
            this.playerIndex = 0;
            this.score = 0;
            this.generatePlayers();
        }

        toString() {
            return this.name;
        }

        gainRun() {
            this.score++;
            logger.log(`${this} now have ${this.score} runs`);
        }

        generatePlayers() {
            for (let i = 0; i < numberOfPlayers; ++i) {
                this.players.push(new Player(`${this} Player ${i + 1}`, this));
            }

            this.pitcher = new Pitcher(`${this} Pitcher`, this);
        }

        nextPlayer() {
            this.playerIndex %= numberOfPlayers;

            let nextPlayer = this.players[this.playerIndex++];

            logger.log(`${nextPlayer} is up to bat`);

            return nextPlayer;
        }

        pitchTo(otherTeam) {
            let outs = 0;
            while (outs < 3) {
                let nextPlayer = otherTeam.nextPlayer();
                let strikes = 0;
                let balls = 0;

                while(strikes < 3 && balls < 4) {
                    let pitch = this.pitcher.pitch();
                    let hit = nextPlayer.acceptPitch(strikes, balls);

                    if (hit.out) {
                        outs++;

                        logger.log(`${nextPlayer} hit the ball!`);
                        logger.log(`${nextPlayer} hit a ${hit.name}`);
                        break;
                    }

                    if (hit.bases === 0) {
                        if (hit.strike) {
                            strikes++;
                            logger.log(`${nextPlayer} swung and missed`);
                        } else {
                            if (pitch.strike) {
                                strikes++;
                            } else {
                                balls++;
                            }
                            logger.log(`${nextPlayer} waited and got a ${pitch.name}`);
                        }
                    } else {
                        logger.log(`${nextPlayer} hit the ball!`);
                        this.trigger('Hit', nextPlayer, hit);
                        logger.log(`${nextPlayer} got a ${hit.name}`);
                        break;
                    }
                }

                if (strikes >= 3) {
                    outs++;
                    logger.log(`${nextPlayer} struck out`);
                } else if (balls >=4) {
                    logger.log(`${nextPlayer} walked`);
                    this.trigger('Hit', nextPlayer, {
                        bases: 1,
                    });
                }
            }
        }

        addListener(object) {
            if (!!object.trigger) {
                this.listeners.push(object);
            }
        }

        trigger() {
            this.listeners.forEach((listener) => listener.trigger(...arguments))
        }
    };
})();

class Player {
    constructor(name, team) {
        this.name = name;
        this.team = team;
    }

    toString() {
        return this.name;
    }

    acceptPitch(strikes = 0, balls = 0) {
        const random = Math.floor(Math.random() * 1000);
        const swingRate = 500 - (75 * strikes) + (30 * balls);

        if (random < swingRate) {
            return {
                name: 'swing and a miss',
                bases: 0,
                out: false,
                strike: true,
            };
        } else if (random < 880) { // wait for the pitch or hit into an out
            if (random > 830) {
                return {
                    name: 'fly out',
                    bases: 0,
                    out: true,
                };
            } else if (random > 810) {
                return {
                    name: 'ground out',
                    bases: 0,
                    out: true,
                };
            }

            return {
                name: 'wait',
                bases: 0,
                out: false,
            };
        } else if (random <= 1000) { // hit the ball
            if (random > 940 && random < 970) {
                return {
                    name: 'double',
                    bases: 2,
                    out: false,
                };
            }
            else if (random >= 970 && random < 995) {
                return {
                    name: 'triple',
                    bases: 3,
                    out: false,
                };
            }
            else if(random >= 995){
                return {
                    name: 'homerun',
                    bases: 4,
                    out: false,
                };
            } else {
                return {
                    name: 'single',
                    bases: 1,
                    out: false,
                };
            }
        }
    }
};

class Pitcher extends Player {
    static get pitchTypes() {
        // @todo math this SH*T up
        return [{
            name: 'strike',
            strike: true,
        },{
            name: 'ball',
            strike: false,
        },{
            name: 'strike',
            strike: true,
        },];
    }

    pitch() {
        const types = Pitcher.pitchTypes;
        const random = Math.floor(Math.random() * (types).length);
        logger.log(`${this} threw the ball`);
        return types[random];
    }
};

const Stadium = ((diamondArea) => {
    const diamondDiv = document.getElementById(diamondArea);
    let firstBase = false,
        secondBase = false,
        thirdBase = false,
        home = false;

    return class Stadium {
        constructor(a, b) {
            // @todo checks that a and b are team objects
            this.teams = [a, b];
        }

        firstBase(player) {
            if (firstBase && player) {
                logger.log(`${firstBase} ran to second`);
                this.secondBase(firstBase);
            }
            firstBase = player;
        }

        secondBase(player) {
            if (secondBase && player) {
                logger.log(`${secondBase} ran to third`);
                this.thirdBase(secondBase);
            }
            secondBase = player;
        }

        thirdBase(player) {
            if (thirdBase && player) {
                logger.log(`${thirdBase} ran home`);
                this.home(thirdBase);
            }
            thirdBase = player;
        }

        home(player) {
            player.team.gainRun();
        }

        single(player) {
            this.firstBase(player);
            logger.log(`${player} ran to first base`);
        }

        double(player) {
            this.single(player);
            this.secondBase(player);
            this.firstBase(false);
            logger.log(`${player} ran to second base`);
        }

        triple(player) {
            this.double(player);
            this.thirdBase(player);
            this.secondBase(false);
            logger.log(`${player} ran to third base`);
        }

        homerun(player) {
            this.triple(player);
            this.home(player);
            this.thirdBase(false);
            logger.log(`${player} ran to home base`);
        }

        get baseTypes() {
            return [this.homerun, this.single, this.double, this.triple];
        }

        clear() {
            firstBase = secondBase = thirdBase = false;

            return this;
        }

        playGame(innings) {
            let extraInnings = 0;
            let [teamA, teamB] = this.teams;
            logger.log(`${innings} inning game!`);
            this.listenToTeamActions();
            for (let i = 1; i <= innings; ++i) {
                logger.logPad(`Playing inning ${i} of ${innings}`);
                this.playInning(i);
            }

            while (teamA.score === teamB.score) {
                extraInnings++;
                logger.logPad(`EXTRA INNINGS!`);
                logger.logPad(`Score: ${teamA}: ${teamA.score} to ${teamB}: ${teamB.score}`);

                this.playInning(`EXTRA ${extraInnings}`);
            }

            logger.logPad(`GAME OVER${extraInnings ? ` WITH ${extraInnings} EXTRA INNINGS!` : '!'}`);
            logger.logPad(`Score: ${teamA}: ${teamA.score} to ${teamB}: ${teamB.score}`);
        }

        playInning(index) {
            let [teamA, teamB] = this.teams;

            logger.logPad(`${teamB} are up to bat in inning ${index}`);
            teamA.pitchTo(teamB);

            this.clear();

            logger.logPad(`${teamA} are up to bat in inning ${index}`);
            teamB.pitchTo(teamA);

            this.clear();
        }

        listenToTeamActions() {
            let [teamA, teamB] = this.teams;

            teamA.addListener(this);
            teamB.addListener(this);
        }

        trigger(event, ...args) {
            // @todo make first letter of event uppercase
            if (!!this[`on${event}`]) {
                this[`on${event}`](...args);
            }
        }

        onHit(player, type) {
            const baseTypes = this.baseTypes;
            const numberOfBases = type.bases % 4;

            baseTypes[numberOfBases].bind(this)(player);
        }
    };
})('diamond');

const mets = new Team('Mets');
const yankees = new Team('Yankees');
const citiField = new Stadium(mets, yankees);
console.log(mets, yankees);
citiField.playGame(9);
\$\endgroup\$
0

1 Answer 1

1
\$\begingroup\$

logger

div.textContent = args.join(', ')

args.join(', ') is going to give you the same value every time. Why keep on doing the same string operation when you could save the output in a variable and just use that?


Team

Your Team function is just a function returning a class except for this one constant variable you define at the very top:

const numberOfPlayers = 9;

It would be simpler to just create the class and put this as a property of the class.

Same thing goes for Stadium. Those variables are states of the stadium and should therefore be apart of the class.



Action class

If you aren't familiar with Redux/Flux, that's okay. If you are, then your creation of these objects that define types of hits and runs look an awful lot like actions in Redux.

That being said, you could simplify the creation of these objects with a class (example):

class Hit {
    construtor(name, bases, out, strike = false) {
        ...
    }
}

Then, you can just use this class rather than re-writing out the object format each time.

Going back to Redux, your program would probably be a lot cleaner if you try integrating Redux into it.


let firstBase = false,
    secondBase = false,
    thirdBase = false,
    home = false;

When seen without context, one would expect these variables to hold boolean values. However, this is obviously not the case:

firstBase = player;

It would make more sense to set these variables to null.


Merging hit type methods

You have three methods single, double, triple, and homerun that could potentially be merged together into one function; you could pass in parameters to denote where players are moved from.

However, this might complicate things. It's up to you.


acceptPitch logic simplification

First off, why do you have this:

} else if (random <= 1000) { // hit the ball

You generated a number between 0 and 1000 in the first place, so this conditional is always going to pass.


**Note: I might've messed up something in my train of thought in this section.*

The first step to simplifying this logic would be to get a list of all the ranges for random you are checking:

  • random < swingRate
  • random < 880
  • random > 830
  • random > 810
  • random > 940 && random < 970
  • random >= 970 && random < 995
  • random >= 995

Now, let's order them from greatest to least (ignoring the first one):

  • random >= 995
  • random >= 970 && random < 995
  • random > 940 && random < 970
  • random < 880
  • random > 830
  • random > 810

Now, let's simplify the conditions. All the ones with arrows next to them can be removed either entirely or partially because, if the conditionals are checked in this order top-to-bottom, then they are redundant and unnecessary.

  • random >= 995
  • random >= 970 && random < 995 <-- previous conditional didn't pass, so second part of this can be removed
  • random > 940 && random < 970 <-- previous conditional didn't pass, so second part of this can be removed
  • random < 880 <-- next conditionals are already less than 880
  • random > 830
  • random > 810

Here's the simplified list, now:

  • random >= 995
  • random >= 970
  • random > 940
  • random > 830
  • random > 810

Now all you need to do is go down these conditionals with if/else ifs, returning the proper hit type as you go.

But, you can simplify it even more. If you don't care about if it's > or >=, then you can create a map that looks like this:

{
    range: pitch_object,
    range: pitch_object,
    ...
}

Or, with a specific example:

{
    995: {
        name: 'homerun',
        bases: 4,
        out: false,
    },
    ...
}

Then, all you need to do is iterate through this map with a for/in loop, checking random with the key and returning the object if random is >/>=:

for(var chance in mapName) {
    if(!mapName.hasOwnProperty(chance)) { continue; }

    if(random >= chance) {
        return mapName[chance];
    }
}
\$\endgroup\$
7
  • \$\begingroup\$ The local this.delay is always getting increased by the logDelay on every log so that the next log comes out at a later point (otherwise everything would get logged all at once) so that was intentional. \$\endgroup\$
    – Naftali
    Commented Apr 12, 2016 at 13:30
  • \$\begingroup\$ The Hit action is a great idea! I am going to try to implement something like that :-) \$\endgroup\$
    – Naftali
    Commented Apr 12, 2016 at 13:30
  • \$\begingroup\$ I do not fully understand your map algorithm, unless I am missing something? \$\endgroup\$
    – Naftali
    Commented Apr 12, 2016 at 13:30
  • \$\begingroup\$ Also do you have any ideas on how I can put in other fielders aside from the pitcher? (or is that something I can ask elsewhere?) \$\endgroup\$
    – Naftali
    Commented Apr 12, 2016 at 13:31
  • \$\begingroup\$ Also my logic for pitch types I think needs improvement as it is a 2/3 chance which the pitcher will throw a strike (which does not seem that "random" to me) no? \$\endgroup\$
    – Naftali
    Commented Apr 12, 2016 at 13:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.