<commit_before>import Axios, { AxiosError } from 'axios';
import { GameProvider } from 'game/providers/base';
import { Actions, Game, GameData, GameMap } from '@battles/models';
import { LocalGameProvider } from './local';
import { ViewData } from 'game/stores/phaser';

type PlayerActionRecord = {
  actions: Actions.ModelAction[];
  updatedAt: number;
};

const BATTLES_API_HOSTNAME = 'https://18vjoshjme.execute-api.ap-southeast-2.amazonaws.com';

export class APIGameProvider extends GameProvider {
  localGameProvider: LocalGameProvider;
  playerActionStorage: PlayerActionLocalStorage;

  cachedGameData: GameData;

  constructor(gameId: string, userId: string) {
    super(gameId, userId);

    this.localGameProvider = LocalGameProvider.createProvider(gameId, userId);
    this.playerActionStorage = new PlayerActionLocalStorage(gameId, userId);
  }

  public async get(): Promise<Game> {
    const gameResponse = await Axios.get(`${BATTLES_API_HOSTNAME}/game/${this.gameId}`);
    this.cachedGameData = gameResponse.data as GameData;

    const actions = await this.findLatestActions();

    const game = new Game(this.cachedGameData);
    const map = new GameMap(game.latestMap);
    for (const action of actions) {
      map.applyAction(action);
    }

    return game;
  }

  public async getViewData(): Promise<ViewData> {
    const viewResponse = await Axios.get(`${BATTLES_API_HOSTNAME}/game/${this.gameId}/view`);
    return viewResponse.data as ViewData;
  }

  public async action(action: Actions.ModelAction): Promise<Game> {
    this.playerActionStorage.addAction(action);
    if (action.type == 'ready-player') {
      await this.pushLatestActions();
    }
    const game = new Game(this.cachedGameData);
    const map = new GameMap(game.latestMap);
    map.applyAction(action);
    return game;
  }

  public wait(condition: (game: Game) => boolean): Promise<Game> {
    throw new Error('Method not implemented.');
  }

  private async pushLatestActions() {
    const actions = this.playerActionStorage.currentActions.actions;
    const playerId = this.userId; // the API provider expects that user id == player id
    const response = await Axios.put(`${BATTLES_API_HOSTNAME}/game/${this.gameId}/actions/${playerId}`, actions);

    if (response.status != 200) throw new Error(JSON.stringify(response));
  }

  private async findLatestActions(): Promise<Actions.ModelAction[]> {
    let apiActionsRecord: PlayerActionRecord;
    try {
      const apiActionsResponse = await Axios.get(`${BATTLES_API_HOSTNAME}/game/${this.gameId}/actions/${this.userId}`);
      apiActionsRecord = apiActionsResponse.data as PlayerActionRecord;
    } catch (e) {
      apiActionsRecord = {
        actions: [],
        updatedAt: 0,
      };
    }

    const localActionsRecord = this.playerActionStorage.currentActions;

    if (apiActionsRecord.updatedAt > localActionsRecord.updatedAt) {
      this.playerActionStorage.saveActions(apiActionsRecord.actions);
      return apiActionsRecord.actions;
    } else {
      return localActionsRecord.actions;
    }
  }
}

const KEY_PREFIX: string = 'graph-battles-actions-';

export class PlayerActionLocalStorage {
  gameId: string;
  playerId: string;

  static KEY_PREFIX: string = 'graph-battles-actions';

  constructor(gameId: string, playerId: string) {
    if (!window) throw new Error('LocalStorage only available in the browser!');
    if (!window.localStorage) throw new Error('LocalStorage not available!');

    this.gameId = gameId;
    this.playerId = playerId;
  }

  addAction(action: Actions.ModelAction) {
    const currentActions = this.currentActions.actions;
    currentActions.push(action);
    this.saveActions(currentActions);
  }

  saveActions(actions: Actions.ModelAction[]) {
    const record: PlayerActionRecord = {
      actions,
      updatedAt: Date.now(),
    };
    window.localStorage.setItem(this.key, JSON.stringify(record));
  }

  get currentActions(): PlayerActionRecord {
    const value = window.localStorage.getItem(this.key);
    if (value === null)
      return {
        actions: [],
        updatedAt: 0,
      };
    const record: PlayerActionRecord = JSON.parse(value);
    return record;
  }

  private get key() {
    return `${KEY_PREFIX}-${this.gameId}-${this.playerId}`;
  }
}<commit_msg>Refactor API calls into a class, add method for game summaries<commit_after>import Axios, { AxiosError } from 'axios';
import { GameProvider } from 'game/providers/base';
import { Actions, Game, GameData, GameMap } from '@battles/models';
import { LocalGameProvider } from './local';
import { ViewData } from 'game/stores/phaser';

type PlayerActionRecord = {
  actions: Actions.ModelAction[];
  updatedAt: number;
};

export class APIGameProvider extends GameProvider {
  localGameProvider: LocalGameProvider;
  playerActionStorage: PlayerActionLocalStorage;
  gameApi: GameAPI;

  cachedGameData: GameData;

  constructor(gameId: string, userId: string) {
    super(gameId, userId);

    this.localGameProvider = LocalGameProvider.createProvider(gameId, userId);
    this.playerActionStorage = new PlayerActionLocalStorage(gameId, userId);
    this.gameApi = new GameAPI();
  }

  public async get(): Promise<Game> {
    this.cachedGameData = await this.gameApi.getGameData(this.gameId);

    const actions = await this.findLatestActions();

    const game = new Game(this.cachedGameData);
    const map = new GameMap(game.latestMap);
    for (const action of actions) {
      map.applyAction(action);
    }

    return game;
  }

  public async getViewData(): Promise<ViewData> {
    return this.gameApi.getViewData(this.gameId);
  }

  public async action(action: Actions.ModelAction): Promise<Game> {
    this.playerActionStorage.addAction(action);
    if (action.type == 'ready-player') {
      await this.pushLatestActions();
    }
    const game = new Game(this.cachedGameData);
    const map = new GameMap(game.latestMap);
    map.applyAction(action);
    return game;
  }

  public wait(condition: (game: Game) => boolean): Promise<Game> {
    throw new Error('Method not implemented.');
  }

  private async pushLatestActions() {
    const actions = this.playerActionStorage.currentActions.actions;
    const playerId = this.userId; // the API provider expects that user id == player id
    await this.gameApi.putPlayerActions(this.gameId, playerId, actions);
  }

  private async findLatestActions(): Promise<Actions.ModelAction[]> {
    let apiActionsRecord: PlayerActionRecord;
    try {
      apiActionsRecord = await this.gameApi.getPlayerActions(this.gameId, this.userId);
    } catch (e) {
      apiActionsRecord = {
        actions: [],
        updatedAt: 0,
      };
    }

    const localActionsRecord = this.playerActionStorage.currentActions;

    if (apiActionsRecord.updatedAt > localActionsRecord.updatedAt) {
      this.playerActionStorage.saveActions(apiActionsRecord.actions);
      return apiActionsRecord.actions;
    } else {
      return localActionsRecord.actions;
    }
  }
}

const KEY_PREFIX: string = 'graph-battles-actions-';

export class PlayerActionLocalStorage {
  gameId: string;
  playerId: string;

  static KEY_PREFIX: string = 'graph-battles-actions';

  constructor(gameId: string, playerId: string) {
    if (!window) throw new Error('LocalStorage only available in the browser!');
    if (!window.localStorage) throw new Error('LocalStorage not available!');

    this.gameId = gameId;
    this.playerId = playerId;
  }

  addAction(action: Actions.ModelAction) {
    const currentActions = this.currentActions.actions;
    currentActions.push(action);
    this.saveActions(currentActions);
  }

  saveActions(actions: Actions.ModelAction[]) {
    const record: PlayerActionRecord = {
      actions,
      updatedAt: Date.now(),
    };
    window.localStorage.setItem(this.key, JSON.stringify(record));
  }

  get currentActions(): PlayerActionRecord {
    const value = window.localStorage.getItem(this.key);
    if (value === null)
      return {
        actions: [],
        updatedAt: 0,
      };
    const record: PlayerActionRecord = JSON.parse(value);
    return record;
  }

  private get key() {
    return `${KEY_PREFIX}-${this.gameId}-${this.playerId}`;
  }
}

type GameSummary = {
  gameId: string;
  maxTurns: number;
  maxVictoryPoints: number;
  finished: boolean;
  leaderboard: {
    name: string;
    victoryPoints: number;
  }[];
};

export class GameAPI {
  static BATTLES_API_HOSTNAME = 'https://18vjoshjme.execute-api.ap-southeast-2.amazonaws.com';

  endpoint: string;

  constructor() {
    this.endpoint = GameAPI.BATTLES_API_HOSTNAME;
  }

  async listGames(): Promise<GameSummary[]> {
    const response = await Axios.get(`${this.endpoint}/game/_all`);
    return response.data as GameSummary[];
  }

  async getViewData(gameId: string): Promise<ViewData> {
    const viewResponse = await Axios.get(`${this.endpoint}/game/${gameId}/view`);
    return viewResponse.data as ViewData;
  }

  async getGameData(gameId: string): Promise<GameData> {
    const gameResponse = await Axios.get(`${this.endpoint}/game/${gameId}`);
    return gameResponse.data as GameData;
  }

  async getPlayerActions(gameId, playerId): Promise<PlayerActionRecord> {
    const apiActionsResponse = await Axios.get(`${this.endpoint}/game/${gameId}/actions/${playerId}`);
    return apiActionsResponse.data as PlayerActionRecord;
  }

  async putPlayerActions(gameId, playerId, actions) {
    const response = await Axios.put(`${this.endpoint}/game/${gameId}/actions/${playerId}`, actions);
    if (response.status != 200) throw new Error(JSON.stringify(response));
  }
}
{'fraction_non_alphanumeric': 0.11991679667186687, 'fraction_numerical': 0.0016640665626625065, 'mean_word_length': 3.9414182939362794, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 21, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4725734', 'n_tokens_mistral': 2874, 'n_tokens_neox': 2864, 'n_words': 634}