Typescript


Correction du Mini-Projet TypeScript

Étapes 1 & 2 — Modélisation et Puissance

Nous définissons les types de base et utilisons l’union discriminante via l’attribut role pour gérer les spécificités de chaque classe.

type BasePlayer = {
  readonly id: number;
  readonly name: string;
  readonly level: number;
  readonly hp: number;
};

type Warrior = BasePlayer & {
  readonly role: "WARRIOR";
  readonly strength: number;
};

type Mage = BasePlayer & {
  readonly role: "MAGE";
  readonly mana: number;
};

type Player = Warrior | Mage;

const displayPlayer = (player: Player): void => {
  switch (player.role) {
    case "WARRIOR":
      console.log(`[Guerrier] ${player.name} (Force: ${player.strength})`);
      break;
    case "MAGE":
      console.log(`[Mage] ${player.name} (Mana: ${player.mana})`);
      break;
  }
};

const getPower = (player: Player): number => {
  switch (player.role) {
    case "WARRIOR":
      return player.level + player.strength;
    case "MAGE":
      return player.level + player.mana;
  }
};

Étapes 3 à 6 — Gestion des Matchs

Pour respecter la contrainte d’immuabilité, chaque modification (comme l’ajout d’un vainqueur) retourne un nouvel objet.

type Match = {
  readonly id: number;
  readonly player1: Player;
  readonly player2: Player;
  readonly winner?: Player;
};

const displayMatch = (match: Match): void => {
  console.log(
    `Match ${match.id}: ${match.player1.name} vs ${match.player2.name}`
  );
  if (match.winner) {
    console.log(`Vainqueur: ${match.winner.name}`);
  }
};

const createMatch = (id: number, player1: Player, player2: Player): Match => {
  if (player1.id === player2.id) {
    throw new Error("Un joueur ne peut pas s'affronter lui-même");
  }
  return { id, player1, player2 };
};

const resolveMatch = (match: Match): Match => {
  if (match.winner) return match;

  const p1Power = getPower(match.player1);
  const p2Power = getPower(match.player2);

  let winner: Player;

  if (p1Power > p2Power) {
    winner = match.player1;
  } else if (p2Power > p1Power) {
    winner = match.player2;
  } else {
    // Égalité de puissance : on tranche par les HP
    winner =
      match.player1.hp >= match.player2.hp ? match.player1 : match.player2;
  }

  return { ...match, winner };
};

const extractWinner = (match: Match): Player => {
  if (!match.winner) {
    throw new Error(`Le match ${match.id} n'a pas encore de vainqueur`);
  }
  return match.winner;
};

Étapes 7 & 8 — Logique du Tournoi

On utilise la récursion pour runTournament afin d’éviter les boucles while et les mutations.

const playRound = (players: readonly Player[]): readonly Player[] => {
  if (players.length % 2 !== 0) {
    throw new Error(
      "Le nombre de joueurs doit être pair pour un tournoi à élimination directe"
    );
  }

  // Création des paires et résolution (0-1, 2-3, etc.)
  return players.reduce<readonly Player[]>((winners, player, index, array) => {
    if (index % 2 === 0) {
      const p1 = player;
      const p2 = array[index + 1];
      const match = createMatch(index, p1, p2);
      const resolvedMatch = resolveMatch(match);
      return [...winners, extractWinner(resolvedMatch)];
    }
    return winners;
  }, []);
};

const runTournament = (players: readonly Player[]): Player => {
  console.log(`--- Nouveau tour : ${players.length} joueurs restants ---`);

  if (players.length === 0) throw new Error("Aucun joueur dans le tournoi");
  if (players.length === 1) return players[0];

  const nextRoundPlayers = playRound(players);
  return runTournament(nextRoundPlayers);
};

Étape 9 — Refactorisation (Union Discriminante de Matchs)

C’est ici que TypeScript brille : on sépare le type Match en deux états distincts.

type BaseMatch = {
  readonly id: number;
  readonly player1: Player;
  readonly player2: Player;
};

type PendingMatch = BaseMatch & { readonly status: "PENDING" };
type FinishedMatch = BaseMatch & {
  readonly status: "FINISHED";
  readonly winner: Player;
};

type MatchV2 = PendingMatch | FinishedMatch;

const createMatchV2 = (
  id: number,
  player1: Player,
  player2: Player
): PendingMatch => {
  if (player1.id === player2.id) throw new Error("ID identiques");
  return { id, player1, player2, status: "PENDING" };
};

const resolveMatchV2 = (match: PendingMatch): FinishedMatch => {
  const p1Power = getPower(match.player1);
  const p2Power = getPower(match.player2);

  const winner = p1Power >= p2Power ? match.player1 : match.player2;

  return {
    ...match,
    status: "FINISHED",
    winner,
  };
};

const extractWinnerV2 = (match: FinishedMatch): Player => {
  // Ici, plus besoin de check "if (!match.winner)",
  // car le type FinishedMatch garantit la présence du winner.
  return match.winner;
};

Exemple d’exécution

const participants: readonly Player[] = [
  { id: 1, name: "Aragorn", level: 10, hp: 100, role: "WARRIOR", strength: 15 },
  { id: 2, name: "Gandalf", level: 10, hp: 80, role: "MAGE", mana: 20 },
  { id: 3, name: "Gimli", level: 8, hp: 120, role: "WARRIOR", strength: 12 },
  { id: 4, name: "Saruman", level: 9, hp: 70, role: "MAGE", mana: 18 },
];

try {
  const winner = runTournament(participants);
  console.log(`🏆 Le grand vainqueur est : ${winner.name}`);
} catch (e) {
  console.error(e);
}