Examen intermédiaire

CS-108 – corrigé

1. Animations continues

Les deux principales difficultés de cet exercice sont d'une part de comprendre l'idée de représenter une animation comme une fonction de trois arguments (les coordonnées \(x\) et \(y\) d'un point du plan, et le temps \(t\)), et d'autre part de correctement gérer la généricité. Le code en lui-même est toujours très court.

Partie 1

Pour obtenir une animation fixe à partir d'une image, il suffit d'appeler la méthode apply de l'image pour obtenir la couleur du point dont la position est donnée, en ignorant totalement le temps.

static <T> Animation<T> ofImage(Image<T> image) {
  return (x, y, t) -> image.apply(x, y);
}

Partie 2

Pour obtenir une animation qui se déplace, on utilise une technique similaire à celle utilisée dans la série sur les images continues pour transformer les images. C'est-à-dire qu'on transforme les coordonnées du point que l'on reçoit pour effectuer une translation, qui ici dépend du temps.

Comme toujours avec ce type de transformation, il faut se souvenir d'appliquer aux coordonnées reçues l'inverse de la transformation que l'on désire appliquer à l'animation, raison pour laquelle on soustrait ci-dessous le temps multiplié par la vitesse aux deux coordonnées.

default Animation<T> moving(double speedX, double speedY) {
  return (x, y, t) -> apply(x - speedX * t, y - speedY * t, t);
}

Partie 3

Pour obtenir l'image d'une animation à un instant donné, il suffit d'utiliser sa méthode apply avec le temps en question comme argument, et les coordonnée passées à la méthode apply de l'image.

default Image<T> snapshot(double t) {
  return (x, y) -> apply(x, y, t);
}

Partie 4

Pour alterner entre deux animations en fonction de si la partie entière du temps est pair, il faut déterminer si c'est le cas ou non, et en fonction de cela, utiliser l'une ou l'autre des deux animations.

default Animation<T> alternatingWith(Animation<T> that) {
  return (x, y, t) -> ((int) t) % 2 == 0
    ? this.apply(x, y, t)
    : that.apply(x, y, t);
}

Il faut noter que, sachant que le temps t est positif ou nul, on peut obtenir sa partie entière au moyen d'une simple conversion en entier. L'utilisation de la méthode Math.floor n'est donc pas nécessaire — mais ne serait pas fausse.

Partie 5

L'utilisation des lambdas obscurcit un peu les patrons de conception qui se cachent derrière les méthodes ci-dessus, mais en réfléchissant un peu on constate que :

  • moving est un décorateur (patron Decorator), car la classe correspondant à la lambda a le type Animation<…>, et elle transforme le comportement d'une autre valeur du même type,
  • alternatingWith est un composite (patron Composite), car elle combine le comportement de deux animations, à savoir this et l'argument de la méthode (that plus haut), pour obtenir une autre animation,
  • ofImage et snapshot sont des adaptateurs (patron Adapter), car elles permettent de transformer une image en animation, ou inversement.

2. Tableau d'affichage

Partie 1

Le constructeur compact doit :

  1. parcourir les points — les valeurs de la table — pour vérifier qu'ils sont tous positifs,
  2. utiliser les méthodes copyOf de Map et List pour copier la table des points et la liste des messages, assurant ainsi l'immuabilité de la classe.
public MessageBoard2 {
  for (int p : points.values())
    if (p <= 0) throw new IllegalArgumentException();

  points = Map.copyOf(points);
  messages = List.copyOf(messages);
}

Partie 2

La seule difficulté de la méthode leaders est qu'il est demandé qu'elle ne parcourt la table des points qu'une seule fois. Elle doit donc, lors de ce parcours, calculer simultanément le nombre maximum de points (maxPoints ci-dessous) et l'ensemble des joueurs les ayant obtenus (leaders ci-dessous).

public Set<PlayerColor> leaders() {
  int maxPoints = 0;
  Set<PlayerColor> leaders = new HashSet<>();
  for (Map.Entry<PlayerColor, Integer> playerPoints
         : points.entrySet()) {
    int points = playerPoints.getValue();
    if (points > maxPoints) {
      leaders.clear();
      leaders.add(playerPoints.getKey());
      maxPoints = points;
    } else if (points == maxPoints) {
      leaders.add(playerPoints.getKey());
    }
  }
  return leaders;
}

Il faut noter que la méthode forEach de Map n'est malheureusement pas utilisable ici, puisqu'il n'est pas possible de modifier une variable locale de la méthode englobante depuis une lambda (maxPoints ci-dessus).

Partie 3

Le bâtisseur contient comme (presque) toujours les mêmes attributs que l'objet qu'il construit, mais ceux-ci sont modifiables. Le code du constructeur et des différentes méthode ne présente pas de difficulté particulière.

public static final class Builder {
  private final Map<PlayerColor, Integer> points;
  private final List<String> messages;

  public Builder(MessageBoard2 initial) {
    this.messages = new ArrayList<>(initial.messages());
    this.points = new HashMap<>(initial.points());
  }

  public void addMessage(String message) {
    messages.add(message);
  }

  public void addMessage(String message,
                         int scoredPoints,
                         Set<PlayerColor> scorers) {
    if (scoredPoints <= 0 || scorers.isEmpty())
      throw new IllegalArgumentException();

    for (PlayerColor player : scorers)
      points.merge(player, scoredPoints, Integer::sum);
    messages.add(message);
  }

  public MessageBoard2 build() {
    return new MessageBoard2(points, messages);
  }
}

Dans la seconde méthode addMessage, il est bien entendu possible de ne pas utiliser merge pour mettre à jour les points. On peut par exemple utiliser put et getOrDefault :

points.put(player,
           points.getOrDefault(player, 0) + scoredPoints);

et plusieurs autres solutions, généralement moins concises.

3. Plateau de jeu

Partie 1

Sachant que le plateau vide a une portée de 0, et que donc le tableau des tuiles a une taille de 1, la définition de EMPTY n'est pas très difficile :

Board2 EMPTY = new Board2(0, new PlacedTile[1], new int[0]);

Partie 2

Le calcul de l'index correspondant à une position (x, y) se fait de la même manière que dans le projet, la seule différence étant que la portée est ici un paramètre de la méthode et pas une constante.

private static int indexFor(int reach, int x, int y) {
  return (-reach <= x && x <= reach
          && -reach <= y && y <= reach)
    ? (y + reach) * sideLength(reach) + (x + reach)
    : -1;
}

Partie 3

La méthode withNewTile doit distinguer deux cas, en fonction de si la portée du plateau courant est suffisante pour la nouvelle tuile ou non. Dans le code ci-dessous, cette vérification est faite en regardant si indexFor retourne -1 ou non.

Si la portée est suffisante, alors withNewTile doit simplement :

  • copier le tableau des index en augmentant sa taille de 1, afin de pouvoir y ajouter l'index de la nouvelle tuile,
  • copier le tableau des tuiles afin d'insérer la nouvelle tuile dans cette copie,
  • retourner une nouvelle instance de Board2 avec ces deux tableaux et la portée actuelle.

Lorsque la portée est insuffisante, la taille du tableau des tuiles doit augmenter. Cela implique que les index des différentes tuiles déjà posées dans ce tableau vont changer, et donc que le tableau des index des tuiles doit être totalement reconstruit.

Une manière de reconstruire ce tableau des index consiste à parcourir l'ancien (indices), utiliser la division entière et son reste pour déterminer la position de chaque tuile déjà posée (x0 et y0 ci-dessous) et finalement calculer leur nouvel index (i1 ci-dessous).

public Board2 withNewTile(PlacedTile newTile, int x, int y) {
  int maybeIndex = indexFor(reach, x, y);
  if (maybeIndex != -1) {
    int[] indices1 =
      Arrays.copyOf(indices, indices.length + 1);
    indices1[indices.length] = maybeIndex;

    PlacedTile[] tiles1 = tiles.clone();
    tiles1[maybeIndex] = newTile;
    return new Board2(reach, tiles1, indices1);
  } else {
    int reach1 = Math.max(Math.abs(x), Math.abs(y));
    int sideLength1 = sideLength(reach1);
    PlacedTile[] tiles1 =
      new PlacedTile[sideLength1 * sideLength1];
    int[] indices1 = new int[indices.length + 1];

    int sideLength = sideLength(reach);
    for (int j = 0; j < indices.length; j += 1) {
      int i = indices[j];
      int x0 = (i % sideLength) - reach;
      int y0 = (i / sideLength) - reach;
      int i1 = indexFor(reach1, x0, y0);
      tiles1[i1] = tiles[i];
      indices1[j] = i1;
    }
    int newTileIndex = indexFor(reach1, x, y);
    indices1[indices.length] = newTileIndex;
    tiles1[newTileIndex] = newTile;
    return new Board2(reach1, tiles1, indices1);
  }
}