Source

core/src/Games/TaxiGame/TaxiGame.ts

import TaxiAction, { TaxiActionKey } from "./Action";
import TaxiGameState from "./GameState";
import * as TaxiGlobals from "./Globals";
import * as TaxiUtils from "./TaxiUtils";
import TaxiCustomer from "./Customer";
import TaxiPlayer from "./Player";
import Vec2 from "../../Utils/Vec2";
import StepResult from "../../RLInterface/StepResult";
import seedrandom from "seedrandom";
import { CustomerStartState } from "./TaxiUtils";

/**
 * The Taxi Game class
 * @param {?number} randomSeed the random seed
 * @category Games
 * @subcategory Taxi
 */
class TaxiGame {
  private static readonly _gameStateDim: number[] = [5, 5, 4, 5];

  /**
   * Mapping of actions
   * @type {Map<TaxiActionKey, TaxiAction>}
   * @readonly
   */
  public static readonly actionMapping: Map<TaxiActionKey, TaxiAction> =
    new Map([
      ["Up", TaxiAction.Up],
      ["Down", TaxiAction.Down],
      ["Left", TaxiAction.Left],
      ["Right", TaxiAction.Right],
      ["PickUp", TaxiAction.PickUp],
      ["DropOff", TaxiAction.DropOff],
    ]);

  private _player: TaxiPlayer;
  private _customer: TaxiCustomer;
  private _rng: seedrandom.PRNG;
  private _isTerminal: boolean = false;
  private _points: number = 0;
  private _iteration: number = 0;

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

  constructor(randomSeed?: number) {
    if (randomSeed) {
      this._rng = seedrandom(randomSeed.toString());
    } else {
      this._rng = seedrandom();
    }
  }

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

  /**
   * Set the games state dimension
   * @type {number[]}
   */
  public static get gameStateDim(): number[] {
    return TaxiGame._gameStateDim;
  }

  /**
   * Get the customer
   * @type {TaxiCustomer}
   */
  public get customer(): TaxiCustomer {
    return this._customer;
  }

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

  /**
   * Get the games current state
   * @type {TaxiGameState}
   */
  public get gameState(): TaxiGameState {
    return {
      playerPos: this._player.position.copy(),
      destinationIdx: this._customer.destIdx,
      customerPosIdx: this.getEncodedCustomerPos(),
    };
  }

  /**
   * Get the games current return
   * @type {number}
   */
  public get return(): number {
    return Number(this._points);
  }

  /**
   * Get whether the game has reached a terminal state
   * @type {boolean}
   */
  public get isTerminal(): boolean {
    return this._isTerminal;
  }

  /**
   * Continue the current game
   * @return {void}
   */
  public continue(): void {
    this._isTerminal = false;
  }

  /**
   * Initialize the game objects
   * @returns {void}
   */
  public initGame(): void {
    this.spawnGameElements();
  }

  /**
   * Update the number of points
   * @param {number} points The points to add / subtract
   * @returns {void}
   */
  public updatePoints(points: number): void {
    this._points += points;
  }

  /**
   * Terminate the game
   * @returns {void}
   */
  public terminateGame(): void {
    this._isTerminal = true;
  }

  /**
   * Spawn the player and customer object
   * @returns {void}
   */
  public spawnGameElements(): void {
    const playerPos: Vec2 = TaxiUtils.getRandomPosition(this._rng);
    this._player = new TaxiPlayer(playerPos, TaxiAction.Down);

    const customerInfo: CustomerStartState = TaxiUtils.resetCustomer(
      this._rng,
      playerPos
    );
    this._customer = new TaxiCustomer(
      customerInfo.spawnIdx,
      customerInfo.destIdx
    );
  }

  /**
   * Reset the game
   * @param {boolean} [resetGameState=true] Whether to reset the games state
   * @param {?TaxiGameState} initialGameState The initial game state
   * @returns {boolean} Whether the reset was successful
   */
  public reset(
    resetGameState: boolean = true,
    initialGameState?: TaxiGameState
  ): boolean {
    this.spawnGameElements();
    if (initialGameState) {
      this._player.position = initialGameState.playerPos.copy();
      this._customer.setNewPosition(
        initialGameState.customerPosIdx,
        initialGameState.destinationIdx
      );
    }
    if (resetGameState) {
      this._points = 0;
      this._isTerminal = false;
      this._iteration = 0;
    } else {
      this._isTerminal = false;
    }
    return true;
  }

  /**
   * Perform a single game step
   * @param {TaxiActionKey} actionString - The action to perform.
   * @returns {StepResult} The result of the step
   */
  public step(actionString: TaxiActionKey): StepResult<TaxiGameState> {
    const action: TaxiAction | undefined =
      TaxiGame.actionMapping.get(actionString);
    if (action === undefined) {
      throw Error("Illegal Action");
    }
    this.incrementIterations();
    let stepResult: StepResult<TaxiGameState>;
    if (action === TaxiAction.DropOff) {
      stepResult = this.dropOffCustomer();
    } else if (action === TaxiAction.PickUp) {
      stepResult = this.pickUpCustomer();
    } else {
      stepResult = this.updatePlayerPosition(action);
    }
    return stepResult;
  }

  private getEncodedCustomerPos(): number {
    if (this.customer.isCustomerPickedUp) {
      return 4;
    }
    return this.customer.spawnDestIdx;
  }

  private dropOffCustomer(): StepResult<TaxiGameState> {
    let reward: number;
    if (
      this._player.position.isEqual(
        TaxiGlobals.destinations[this.customer.destIdx]
      ) &&
      this._customer.isCustomerPickedUp
    ) {
      this.updatePoints(TaxiGlobals.dropOffPassangerPoints);
      this._customer.dropOffCustomer();
      this.terminateGame();
      reward = TaxiGlobals.dropOffPassangerPoints;
    } else {
      this.updatePoints(TaxiGlobals.illegalMovePoints);
      reward = TaxiGlobals.illegalMovePoints;
    }
    return {
      newState: this.gameState,
      reward: reward,
    };
  }

  private pickUpCustomer(): StepResult<TaxiGameState> {
    let reward: number;
    if (
      !this._customer.isCustomerPickedUp &&
      this._player.position.isEqual(this.customer.position)
    ) {
      this._customer.pickUpCustomer();
      this.updatePoints(TaxiGlobals.stepPenaltyPoints);
      reward = TaxiGlobals.stepPenaltyPoints;
    } else {
      this.updatePoints(TaxiGlobals.illegalMovePoints);
      reward = TaxiGlobals.illegalMovePoints;
    }
    return {
      newState: this.gameState,
      reward: reward,
    };
  }

  /**
   * encode the game state to an encoded numbers array
   * @param {TaxiGameState} state the state to encode
   * @returns {number[]} The encoded array
   */
  public static encodeStateToIndices(state: TaxiGameState): number[] {
    return [
      state.playerPos.x,
      state.playerPos.y,
      state.destinationIdx,
      state.customerPosIdx,
    ];
  }

  /**
   * Update the player position with the taken action
   * @param {TaxiAction} action the taken action
   * @returns {StepResult} The result of the steps
   */
  public updatePlayerPosition(action: TaxiAction): StepResult<TaxiGameState> {
    this.updatePoints(TaxiGlobals.stepPenaltyPoints);
    this._player.updatePosition(action);
    return {
      newState: this.gameState,
      reward: TaxiGlobals.stepPenaltyPoints,
    };
  }

  /**
   * Increment the game iteration
   * @returns {void}
   */
  public incrementIterations(): void {
    this._iteration++;
  }

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

  /**
   * Get the random number generator
   * @type {seedrandom.PRNG}
   */
  public get rng(): seedrandom.PRNG {
    return this._rng;
  }
}

export default TaxiGame;