Source

core/src/Games/BlackJack/BackJackGame.ts

import StepResult from "../../RLInterface/StepResult";
import seedrandom from "seedrandom";
import BlackJackAction, { BlackJackActionKey } from "./Action";
import BlackJackPlayer from "./Player";
import BlackJackDealer from "./Dealer";
import BlackJackGameState from "./GameState";
import BlackJackCard from "./Card";

/**
 * The Black Jack implementation is oriented on the logic described in Richard
 * S. Sutton and Andrew G. Barto 'Reinforcement Learning: An Introduction' Example 5.1..
 * @param {?number} randomSeed the random seed
 * @category Games
 * @subcategory BlackJack
 */
class BlackJackGame {
  /**
   * The action mapping
   * @static
   * @type {Map<BlackJackActionKey, BlackJackAction>}
   * @readonly
   */
  public static readonly actionMapping: Map<
    BlackJackActionKey,
    BlackJackAction
  > = new Map([
    ["Stick", BlackJackAction.Stick],
    ["Hit", BlackJackAction.Hit],
  ]);

  public static readonly _gameStateDim: number[] = [32, 11, 2];

  private _rng: seedrandom.PRNG;
  private _player: BlackJackPlayer;
  private _dealer: BlackJackDealer;
  private _iteration: number = 0;

  constructor(randomSeed?: number) {
    if (randomSeed) {
      this._rng = seedrandom(randomSeed.toString());
    } else {
      this._rng = seedrandom();
    }
    this._player = new BlackJackPlayer(this._rng);
    this._dealer = new BlackJackDealer(this._rng);
  }

  /**
   * Get the game state dimension
   * @type {number[]}
   */
  public static get gameStateDim(): number[] {
    return BlackJackGame._gameStateDim;
  }

  /**
   * Get the player
   * @type {BlackJackPlayer}
   */
  public get player(): BlackJackPlayer {
    return this._player;
  }

  /**
   * Get the dealer
   * @type {BlackJackDealer}
   */
  public get dealer(): BlackJackDealer {
    return this._dealer;
  }

  /**
   * Get the action space
   * @type {string[]}
   */
  public static get actionSpace(): string[] {
    return Array.from(BlackJackGame.actionMapping.keys());
  }

  /**
   * Get The return
   * @type {number}
   */
  public get return(): number {
    if (!this.isTerminal) return 0;
    if (this._player.score > 21) return -1;
    if (this._dealer.score > 21) return 1;
    const playerScore = Math.abs(this._player.score - 21);
    const dealerScore = Math.abs(this._dealer.score - 21);
    if (playerScore < dealerScore) return 1;
    if (playerScore > dealerScore) return -1;
    return 0;
  }

  /**
   * Return whether the game has terminated
   * @type {boolean}
   */
  public get isTerminal(): boolean {
    return this._dealer.sticks && this._player.sticks;
  }

  /**
   * Get the games state
   * @type {BlackJackGameState}
   */
  public get gameState(): BlackJackGameState {
    return {
      playerScore: this._player.score,
      shownCard: this._dealer.shownCard,
      playerHoldsUsableAce: this._player.hasUsableAce,
    };
  }

  /**
   * Get the iteration
   * @type {number}
   */
  public get iteration(): number {
    return this._iteration;
  }

  /**
   * initialize the game
   * @returns {void}
   */
  public initGame(): void {
    this._player.init();
    this._dealer.init();
    const playerHasNat = this.playerHasNatural();
    if (playerHasNat) {
      this.endGame();
    }
  }

  /**
   * Set the random seed
   * @type {number}
   */
  public set randomSeed(randomSeed: number) {
    this._rng = seedrandom(randomSeed.toString());
    this.reset(true);
  }

  /**
   * Return whether the player has a natural blackjack
   * @returns {boolean} whether the player has a natural blackjack
   */
  public playerHasNatural(): boolean {
    if (this._player.score === 21) return true;
    return false;
  }

  /**
   * Make an environment step
   * @param {BlackJackActionKey} actionString The action
   * @returns {StepResult} The result
   */
  public step(
    actionString: BlackJackActionKey
  ): StepResult<BlackJackGameState> {
    this._iteration++;
    const action: BlackJackAction | undefined =
      BlackJackGame.actionMapping.get(actionString);

    if (action === undefined) {
      throw Error("Illegal Action");
    }

    switch (action) {
      case BlackJackAction.Hit:
        const newCard = BlackJackCard.returnRandomCard(this._rng);
        this._player.addCard(newCard);
        if (this._player.score > 21) {
          this.endGame();
        }
        break;
      case BlackJackAction.Stick:
        this._player.callStick();
        this.simulateDealer();
        break;
    }

    return {
      newState: this.gameState,
      reward: this.return,
    };
  }

  /**
   * Simulate the dealer
   * @returns {void}
   */
  public simulateDealer(): void {
    while (!this._dealer.sticks) {
      this._dealer.act();
    }
  }

  /**
   * Encode the state to a number encoded array
   * @param {BlackJackGameState} state the state
   * @returns the encoded array
   */
  public static encodeStateToIndices(state: BlackJackGameState): number[] {
    return [
      state.playerScore,
      state.shownCard!.value,
      Number(state.playerHoldsUsableAce),
    ];
  }

  /**
   * End the game
   * @returns {void}
   */
  public endGame(): void {
    this._player.callStick();
    this._dealer.callStick();
  }

  /**
   * Reset the game
   * @param {boolean} [reinit=true] Whether to reinitialize the game
   * @returns {boolean}
   */
  public reset(reinit = true): boolean {
    this._iteration = 0;
    this._player.reset();
    this._dealer.reset();
    if (reinit) {
      this.initGame();
    }
    return true;
  }
}

export default BlackJackGame;