All files / app/services/game-simulator game-simulator.service.ts

96.96% Statements 96/99
84.61% Branches 11/13
66.66% Functions 2/3
96.96% Lines 96/99

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 1001x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 5x 5x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 2x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 1x 1x 1x 1x 2x 1x 1x 1x 1x 2x 1x 1x 1x 1x       1x 1x 1x 3x 3x 3x 3x 3x 1x 1x  
import {Injectable, computed, inject, signal} from '@angular/core';
import {Game} from "../../models/game";
import {Team} from "../../models/team";
import {JobScheduler} from "../job-scheduler/job-scheduler.service";
import { Role } from '../../models/role';
 
/**
 * This service exposes the state of the application in the form of signals.
 * Some signals are settings for the simulation and can be set. 
 * Others are computed signals and provide statistics about the games already simulated.
 * A background process continuosly simulates games and update the state.
 */
@Injectable({
  providedIn: 'root'
})
export class GameSimulator {
 
  scheduler = inject(JobScheduler);
 
  constructor() {
    this.scheduler.addJob(this.backgroundJob);
  }
 
  /** The game setup that is being simulated. Simulations restart when this is modified. */
  game = signal(new Game(10, [
    {role: Role.WEREWOLF, count: 2},
    {role: Role.GUARD, count: 2},
    {role: Role.HEALER, count: 1},
  ]));
 
  /** All the games simulated. They have ended with a winner, number of days, etc. */
  results = signal<Game[]>([]);
 
  /** @readonly The percent of games won by the Village team */
  winRate = computed(() => {
    const results = this.results();
    const wins = results.filter(game => game.winner == Team.VILLAGE).length;
    return wins / results.length;
  })
 
  /** 
   * @readonly 
   * More detailed stats for the games simulated.
   * The map keys are the number of days that the game lasted. 
   * The values are the percentage of games won by each of the teams.
   */
  stats = computed(() => {
    const results = this.results();
    const totalGames = results.length;
    let longestGame = 0;
    let shortestGame = Infinity;
    const winCounts = new Map<number, Map<Team, number>>();
    for (const game of results) {
      const duration = game.days;
      if (duration > longestGame) longestGame = duration;
      if (duration < shortestGame) shortestGame = duration;
      const count = winCounts.get(duration) || new Map<Team, number>();
      const team = game.winner;
      if (team === undefined) continue;
      const wins = count.get(team) || 0;
      count.set(team, wins + 1);
      winCounts.set(duration, count);
    }
    const stats = new Map<number, Map<Team, number>>();
    for (let days = shortestGame; days <= longestGame; days++) {
      const villageWinCount = winCounts.get(days)?.get(Team.VILLAGE) || 0;
      const wolvesWinCount = winCounts.get(days)?.get(Team.WOLVES) || 0;
      const villageWinRate = villageWinCount / totalGames;
      const wolvesWinRate = wolvesWinCount / totalGames;
      stats.set(days, new Map([[Team.VILLAGE,villageWinRate],[Team.WOLVES,wolvesWinRate]]));
    }
    return stats;
  })
 
  /** @readonly The number of players in the settings */
  countPlayers = computed(() => {
    return this.game().players;
  });
 
  /** @readonly The number of players that are against the village team */
  countEvil = computed(() => {
    return this.game().teamCount(Team.WOLVES);
  });
 
  /** Restarts the simulation with new game settings, erasing all statistics */
  restart(game: Game) {
    this.game.set(game);
    this.results.set([]);
  }
 
  /** Job to run using the job scheduler */
  backgroundJob = () => {
    if (this.results().length >= 10000) return;
    const game = this.game().clone();
    while (!game.ended) game.advance();
    this.results.update(results => [...results, game]);
  }
 
}