From f19b688112598273ab5e1147c185c4ab2a3fe749 Mon Sep 17 00:00:00 2001 From: Pacien TRAN-GIRARD Date: Sun, 8 May 2016 20:26:28 +0200 Subject: Refactor deserialization --- src/ch/epfl/xblast/client/GameState.java | 70 +++-- .../epfl/xblast/client/GameStateDeserializer.java | 283 ++++++++++----------- src/ch/epfl/xblast/client/ImageCollection.java | 154 ++++++++--- .../epfl/xblast/client/painter/ScorePainter.java | 107 ++++++++ .../xblast/client/painter/TimeLinePainter.java | 39 +++ 5 files changed, 440 insertions(+), 213 deletions(-) create mode 100644 src/ch/epfl/xblast/client/painter/ScorePainter.java create mode 100644 src/ch/epfl/xblast/client/painter/TimeLinePainter.java (limited to 'src') diff --git a/src/ch/epfl/xblast/client/GameState.java b/src/ch/epfl/xblast/client/GameState.java index 4906094..7652cd5 100644 --- a/src/ch/epfl/xblast/client/GameState.java +++ b/src/ch/epfl/xblast/client/GameState.java @@ -1,31 +1,29 @@ package ch.epfl.xblast.client; +import ch.epfl.xblast.ArgumentChecker; +import ch.epfl.xblast.Lists; import ch.epfl.xblast.PlayerID; import ch.epfl.xblast.SubCell; +import ch.epfl.xblast.client.painter.ScorePainter; +import ch.epfl.xblast.client.painter.TimeLinePainter; import java.awt.*; import java.util.List; +import java.util.Objects; /** + * The client representation of a game state. + * * @author Timothée FLOURE (257420) + * @author Pacien TRAN-GIRARD (261948) */ public final class GameState { - /** - * GameState's parameters. - */ - private final List players; - private final List board; - private final List explosions; - private final List scores; - private final List ticks; /** * A Player. */ public static final class Player { - /** - * Player's parameters. - */ + private final PlayerID id; private final int lives; private final SubCell position; @@ -34,16 +32,16 @@ public final class GameState { /** * Instantiates a new Player. * - * @param id id of the player - * @param lives number of lives of the player + * @param id id of the player + * @param lives number of lives of the player * @param position position of the player - * @param image image related to the actual state of the player + * @param image image related to the actual state of the player */ public Player(PlayerID id, int lives, SubCell position, Image image) { this.id = id; this.lives = lives; - this.position = position; - this.image = image; + this.position = Objects.requireNonNull(position); + this.image = Objects.requireNonNull(image); } /** @@ -73,23 +71,42 @@ public final class GameState { public Image image() { return this.image; } + + } + + private final List players; + private final List board; + private final List explosions; + private final List scores; + private final List ticks; + + /** + * Instantiates a new GameState. + * + * @param players list containing the players + * @param board list containing all the images composing the board + * @param explosions list containing all the images composing the blasts and the bombs + * @param scores list containing all the images composing the actual score + * @param ticks list containing all the images composing the time "line" + */ + public GameState(List players, List board, List explosions, List scores, List ticks) { + this.players = Lists.immutableList(ArgumentChecker.requireNonEmpty(players)); + this.board = Lists.immutableList(ArgumentChecker.requireNonEmpty(board)); + this.explosions = Lists.immutableList(Objects.requireNonNull(explosions)); + this.scores = Lists.immutableList(ArgumentChecker.requireNonEmpty(scores)); + this.ticks = Lists.immutableList(ArgumentChecker.requireNonEmpty(ticks)); } /** * Instantiates a new GameState. * - * @param players list containing the players - * @param board list containing all the images composing the board + * @param players list containing the players + * @param board list containing all the images composing the board * @param explosions list containing all the images composing the blasts and the bombs - * @param scores list containing all the images composing the actual score - * @param ticks list containing all the images composing the time "line" + * @param ticks the ticks */ - public GameState(List players, List board, List explosions, List scores, List ticks) { - this.players = players; - this.board = board; - this.explosions = explosions; - this.scores = scores; - this.ticks = ticks; + public GameState(List players, List board, List explosions, int ticks) { + this(players, board, explosions, ScorePainter.buildScorePanel(players), TimeLinePainter.buildTimeLine(ticks)); } /** @@ -126,4 +143,5 @@ public final class GameState { public List ticks() { return this.ticks; } + } diff --git a/src/ch/epfl/xblast/client/GameStateDeserializer.java b/src/ch/epfl/xblast/client/GameStateDeserializer.java index f1327e3..20b44f9 100644 --- a/src/ch/epfl/xblast/client/GameStateDeserializer.java +++ b/src/ch/epfl/xblast/client/GameStateDeserializer.java @@ -1,164 +1,170 @@ package ch.epfl.xblast.client; +import ch.epfl.xblast.Lists; import ch.epfl.xblast.PlayerID; import ch.epfl.xblast.RunLengthEncoder; import ch.epfl.xblast.SubCell; import ch.epfl.xblast.client.GameState.Player; -import ch.epfl.xblast.server.Ticks; -import java.util.ArrayList; +import java.awt.*; +import java.util.*; import java.util.List; -import java.awt.Image; import java.util.stream.Collectors; /** + * The client game state deserializer. + * + * @author Pacien TRAN-GIRARD (261948) * @author Timothée FLOURE (257420) */ public final class GameStateDeserializer { - /** - * Image collections folders - */ - private final static String BOARD_IMAGES_FOLDER = "block"; - private final static String EXPLOSION_IMAGES_FOLDER = "explosion"; - private final static String PLAYER_IMAGES_FOLDER = "player"; - private final static String TIME_IMAGES_FOLDER = "score"; - private final static String SCORE_IMAGES_FOLDER = "score"; - /** - * Serialized data chunks - */ - private final static int NUMBER_OF_PLAYERS = 4; - private final static int PLAYER_CHUNK_SIZE = 4; - private final static int PLAYER_TOTAL_SIZE = NUMBER_OF_PLAYERS * PLAYER_CHUNK_SIZE; + private static final int SERIALIZED_PLAYER_LENGTH = 4; + private static final int SERIALIZED_PLAYERS_LENGTH = PlayerID.values().length * SERIALIZED_PLAYER_LENGTH; + private static final int SERIALIZED_TICKS_LENGTH = 1; /** - * Scores & Time + * Represents a length-prefixed chunk of byte-encoded data. */ - private final static int TIME_LINE_SIZE = (Ticks.GAME_DURATION / 2); - private final static int SCORE_SEPARATION_SIZE = 8; + private static class VariableLengthChunk { - /** - * Images ID - */ - private final static int TIME_LINE_LED_ON_IMAGE_ID = 21; - private final static int TIME_LINE_LED_OFF_IMAGE_ID = 20; - private final static int SCORE_TEXT_MIDDLE_IMAGE_ID = 10; - private final static int SCORE_TEXT_RIGHT_IMAGE_ID = 11; - private final static int SCORE_TILE_VOID_IMAGE_ID = 12; + private static final int PREFIX_SHIFT = 1; - /** - * Deserialize an encoded chunk of data and link it to images. - * - * @param data serialized data - * @param imageCollection ImageCollection related to the type of data - * @return a list of Images corresponding to the serialized element - */ - private static List deserializeChunk(List data, ImageCollection imageCollection) { - return RunLengthEncoder.decode(data).stream() - .map(c -> imageCollection.imageOrNull(Byte.toUnsignedInt(c))) - .collect(Collectors.toList()); - } + private final List bytes; - /** - * Build a list of players given the serialized data. - * - * @param serializedPlayers the serialized players - * @return a list of players built from the serialized data - */ - private static List deserializePlayers(List serializedPlayers) { - List players = new ArrayList<>(); - - for (int i = 0; i < NUMBER_OF_PLAYERS; i++) { // ugly - players.add( - deserializePlayer( - serializedPlayers.subList(i * PLAYER_CHUNK_SIZE, (i+1) * PLAYER_CHUNK_SIZE), i - ) - ); + /** + * Instantiates a VariableLengthChunk from a byte sequence. + * + * @param l the byte sequence + */ + public VariableLengthChunk(List l) { + this.bytes = Lists.immutableList(l); } - return players; - } + /** + * Returns the length of the chunk given by its prefix. + * + * @return the length of the chunk + */ + public int length() { + if (this.bytes.isEmpty()) + return 0; + else + return Byte.toUnsignedInt(this.bytes.get(0)); + } - /** - * Generate a Player from its serialized data. - * - * @param serializedPlayer a serialized player - * @return a player built from its serialized data - */ - private static Player deserializePlayer(List serializedPlayer, int i) { - PlayerID id = PlayerID.values()[i]; + /** + * Returns the head of the desired length of the chunk. + * + * @param takeFirst the desired length + * @return the head + */ + public List head(int takeFirst) { + return this.bytes.subList(0, takeFirst); + } - int lives = Byte.toUnsignedInt(serializedPlayer.get(0)); + /** + * Returns the head of the size-prefixed chunk. + * + * @return the head + */ + public List head() { + if (this.length() < 1) + return Collections.emptyList(); + else + return this.bytes.subList(PREFIX_SHIFT, this.length() + PREFIX_SHIFT); + } - SubCell position = new SubCell( - Byte.toUnsignedInt(serializedPlayer.get(1)), - Byte.toUnsignedInt(serializedPlayer.get(2)) - ); + /** + * Returns the tail of the chunk, with the head of the given length dropped. + * + * @param dropFirst the length to drop + * @return the tail + */ + public VariableLengthChunk tail(int dropFirst) { + return new VariableLengthChunk( + this.bytes.subList(dropFirst, this.bytes.size())); + } - Image image = (new ImageCollection(PLAYER_IMAGES_FOLDER)).imageOrNull(Byte.toUnsignedInt(serializedPlayer.get(3))); + /** + * Returns the tail of the size-prefixed chunk. + * + * @return the tail + */ + public VariableLengthChunk tail() { + return new VariableLengthChunk( + this.bytes.subList(this.length() + PREFIX_SHIFT, this.bytes.size())); + } - return new Player(id, lives, position, image); + } + + private GameStateDeserializer() { + // Static class } /** - * Generate the list of images composing the scores. + * Deserialize an encoded chunk of data and link it to images. * - * @param players players - * @return the scores of the given players + * @param data serialized data + * @param imageCollection ImageCollection related to the type of data + * @return a list of Images corresponding to the serialized element */ - private static List buildScores(List players) { - List scores = new ArrayList<>(); - ImageCollection imageCollection = new ImageCollection(SCORE_IMAGES_FOLDER); - - scores.addAll(buildPlayerScore(players.get(0), imageCollection)); // PLAYER_1 - scores.addAll(buildPlayerScore(players.get(1), imageCollection)); // PLAYER_2 - - // Separation tiles - for (int i = 0; i < SCORE_SEPARATION_SIZE; i++){ scores.add(imageCollection.imageOrNull(SCORE_TILE_VOID_IMAGE_ID)); } - - scores.addAll(buildPlayerScore(players.get(2), imageCollection)); // PLAYER_3 - scores.addAll(buildPlayerScore(players.get(3), imageCollection)); // PLAYER_4 - - return scores; + private static List deserializeImageChunk(List data, ImageCollection imageCollection) { + return RunLengthEncoder.decode(data).stream() + .map(Byte::toUnsignedInt) + .map(imageCollection::imageOrNull) + .collect(Collectors.toList()); } /** - * Generate the images composing the score of one player. + * Unserializes a byte representation of a Player. * - * @param player serialized data corresponding to the player - * @param imageCollection image collection related to the scores - * @return the score of the given player + * @param id the PlayerID + * @param serializedPlayer the serialized player + * @return the player + * @throws IllegalArgumentException if the byte chunk is invalid. */ - private static List buildPlayerScore(Player player, ImageCollection imageCollection) { - List playerScore = new ArrayList<>(); + private static Player deserializePlayer(PlayerID id, List serializedPlayer) { + if (Objects.isNull(serializedPlayer) || serializedPlayer.size() != SERIALIZED_PLAYER_LENGTH) + throw new IllegalArgumentException(); - if (player.lives() < 1) - playerScore.add(imageCollection.imageOrNull(0 + (player.id().ordinal()*2))); - else - playerScore.add(imageCollection.imageOrNull(1 + (player.id().ordinal()*2))); + List intPlayerData = serializedPlayer.stream() + .map(Byte::toUnsignedInt) + .collect(Collectors.toList()); - playerScore.add(imageCollection.imageOrNull(SCORE_TEXT_MIDDLE_IMAGE_ID)); - playerScore.add(imageCollection.imageOrNull(SCORE_TEXT_RIGHT_IMAGE_ID)); - return playerScore; + Integer lives = intPlayerData.get(0); + SubCell pos = new SubCell(intPlayerData.get(1), intPlayerData.get(2)); + Image img = ImageCollection.PLAYERS_IMAGES.imageOrNull(intPlayerData.get(3)); + return new Player(id, lives, pos, img); } /** - * Generate the list of images composing the time "line". + * Unserializes a byte representation of players. * - * @param time the remaining time - * @return the list of images composing the time "line" + * @param serializedPlayers the serialized players + * @return the list of deserialized players + * @throws IllegalArgumentException if the byte chunk is invalid. */ - private static List buildTimeLine(Byte time) { - List ticks = new ArrayList<>(); - ImageCollection imageCollection = new ImageCollection(TIME_IMAGES_FOLDER); + private static List deserializePlayers(List serializedPlayers) { + if (Objects.isNull(serializedPlayers) || serializedPlayers.size() != SERIALIZED_PLAYERS_LENGTH) + throw new IllegalArgumentException(); + + return Collections.unmodifiableList(Arrays.stream(PlayerID.values()) + .map(pid -> { + List chunk = serializedPlayers.subList( + pid.ordinal() * SERIALIZED_PLAYER_LENGTH, + (pid.ordinal() + 1) * SERIALIZED_PLAYER_LENGTH); + return new AbstractMap.SimpleImmutableEntry<>(pid, chunk); + }) + .map(e -> deserializePlayer(e.getKey(), e.getValue())) + .collect(Collectors.toList())); + } - for (int i = 0; i < TIME_LINE_SIZE; i++) { - if (i < Byte.toUnsignedInt(time)) - ticks.add(imageCollection.imageOrNull(TIME_LINE_LED_ON_IMAGE_ID)); - else - ticks.add(imageCollection.imageOrNull(TIME_LINE_LED_OFF_IMAGE_ID)); - } - return ticks; + private static int deserializeTicks(List serializedTicks) { + if (Objects.isNull(serializedTicks) || serializedTicks.size() != SERIALIZED_TICKS_LENGTH) + throw new IllegalArgumentException(); + + return Byte.toUnsignedInt(serializedTicks.get(0)); } /** @@ -167,36 +173,21 @@ public final class GameStateDeserializer { * @param serializedData the serialized gameState * @return a new GameState build from the serialized data. */ - public static GameState deserialize(List serializedData) { // Ugly - - // Build the indexes (Ugly) - int boardBeginIndex = 0; // First element - int boardEndIndex = (boardBeginIndex) + Byte.toUnsignedInt(serializedData.get(boardBeginIndex)); - int explosionsBeginIndex = boardEndIndex + 1; - int explosionsEndIndex = explosionsBeginIndex + Byte.toUnsignedInt(serializedData.get(explosionsBeginIndex)); - int playersBeginIndex = explosionsEndIndex + 1; - int playersEndIndex = (playersBeginIndex - 1) + PLAYER_TOTAL_SIZE; - Byte time = serializedData.get(playersEndIndex + 1); // Last element - - // Deserialize the Board - List board = deserializeChunk( - serializedData.subList(boardBeginIndex + 1, boardEndIndex + 1), - new ImageCollection(BOARD_IMAGES_FOLDER) - ); - - // Deserialize the explosions - List explosions = deserializeChunk( - serializedData.subList(explosionsBeginIndex + 1,explosionsEndIndex + 1), - new ImageCollection(EXPLOSION_IMAGES_FOLDER) - ); - - // Deserialize the players - List players = deserializePlayers(serializedData.subList(playersBeginIndex,playersEndIndex + 1)); - - // Generate the scores and the time "line" - List scores = buildScores(players); - List timeLine = buildTimeLine(time); - - return new GameState(players, board, explosions, scores, timeLine); + public static GameState deserialize(List serializedData) { + VariableLengthChunk chunks = new VariableLengthChunk(serializedData); + + List board = deserializeImageChunk(chunks.head(), ImageCollection.BOARD_IMAGES); + chunks = chunks.tail(); + + List explosions = deserializeImageChunk(chunks.head(), ImageCollection.EXPLOSIONS_IMAGES); + chunks = chunks.tail(); + + List players = deserializePlayers(chunks.head(SERIALIZED_PLAYERS_LENGTH)); + chunks = chunks.tail(SERIALIZED_PLAYERS_LENGTH); + + int ticks = deserializeTicks(chunks.head(SERIALIZED_TICKS_LENGTH)); + + return new GameState(players, board, explosions, ticks); } + } diff --git a/src/ch/epfl/xblast/client/ImageCollection.java b/src/ch/epfl/xblast/client/ImageCollection.java index ee0e8cf..09ffd59 100644 --- a/src/ch/epfl/xblast/client/ImageCollection.java +++ b/src/ch/epfl/xblast/client/ImageCollection.java @@ -1,86 +1,158 @@ package ch.epfl.xblast.client; import javax.imageio.ImageIO; -import java.util.HashMap; -import java.util.Map; -import java.awt.Image; +import java.awt.*; import java.io.File; -import java.util.NoSuchElementException; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.*; +import java.util.List; +import java.util.stream.Collectors; /** + * An image collection. + * + * @author Pacien TRAN-GIRARD (261948) * @author Timothée FLOURE (257420) */ public final class ImageCollection { + + private static final String FILENAME_SEPARATOR = "_"; + + public final static ImageCollection BOARD_IMAGES = new ImageCollection("block"); + public final static ImageCollection EXPLOSIONS_IMAGES = new ImageCollection("explosion"); + public final static ImageCollection PLAYERS_IMAGES = new ImageCollection("player"); + public final static ImageCollection SCORES_IMAGES = new ImageCollection("score"); + public final static ImageCollection TIME_IMAGES = SCORES_IMAGES; + /** - * Bounds of the file ID in the filename. + * Returns the URL of the requested directory, or raise an Exception if it cannot be accessed. + * + * @param dir the requested directory + * @return the directory URL + * @throws NoSuchElementException if the requested directory is not valid */ - private static final int FILENAME_ID_BEGIN_INDEX = 0; - private static final int FILENAME_ID_END_INDEX = 2; + private static URL getDirectoryURL(String dir) { + URL url = ImageCollection.class + .getClassLoader() + .getResource(dir); + + if (Objects.isNull(url)) + throw new NoSuchElementException(dir); + + return url; + } /** - * Directory related to the ImageCollection. + * Returns the URI of the requested directory, or raise an Exception if it cannot be accessed. + * + * @param dir the requested directory + * @return the directory URI + * @throws NoSuchElementException if the requested directory is not valid + */ + private static URI getDirectoryURI(String dir) { + try { + return getDirectoryURL(dir).toURI(); + } catch (URISyntaxException e) { + throw new NoSuchElementException(dir); + } + } + + /** + * Returns the content of the directory at the given path. + * + * @param dirName the directory + * @return its content + * @throws NoSuchElementException if the given path is unreachable */ - private final File directory; + private static List getDirectoryContent(String dirName) { + File dir = new File(getDirectoryURI(dirName)); + if (!dir.isDirectory()) + throw new NoSuchElementException(dirName); + + File[] files = dir.listFiles(); + if (Objects.isNull(files)) + throw new NoSuchElementException(dirName); + + return Arrays.asList(files); + } /** - * Maps the directory's files with IDs parsed from their names. + * Parses the ID of an image from its name, or returns null on error. * - * @return a map of id to files + * @param filename the name of the file + * @return the ID, or null */ - private Map mapFilesId() { - HashMap mappedFiles = new HashMap<>(); + private static Integer parseImageID(String filename) { + try { + return Integer.parseInt(filename.substring(0, filename.indexOf(FILENAME_SEPARATOR))); + } catch (NumberFormatException | IndexOutOfBoundsException e) { + return null; + } + } - for (File file : this.directory.listFiles()) { - mappedFiles.put(Integer.parseInt(file.getName().substring(FILENAME_ID_BEGIN_INDEX,FILENAME_ID_END_INDEX)), file); + /** + * Reads an image from the given File or returns null on error. + * + * @param file the file + * @return the Image, or null + */ + private static Image readImage(File file) { + try { + return ImageIO.read(file); + } catch (IOException e) { + return null; } + } - return mappedFiles; + /** + * Builds and returns a mapping ID->Image, with all invalid elements discarded. + * + * @param fileList the list of images files + * @return the ID->Image mapping + */ + private static Map buildImageMap(List fileList) { + return fileList.stream() + .map(f -> new AbstractMap.SimpleImmutableEntry<>(parseImageID(f.getName()), readImage(f))) + .filter(e -> !Objects.isNull(e.getKey()) && !Objects.isNull(e.getValue())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } + private final Map images; + /** * Instantiates a new ImageCollection. * + * @param dir the relative directory name * @throws NoSuchElementException if the requested directory is not valid - * @param directory the relative directory name */ - public ImageCollection(String directory) { - try { // Ugly! Probably a better way... - this.directory = new File(ImageCollection.class - .getClassLoader() - .getResource(directory) - .toURI()); - } catch (Exception e) { - throw new NoSuchElementException(); - } + public ImageCollection(String dir) { + this.images = buildImageMap(getDirectoryContent(dir)); } /** * Returns the image corresponding to the given id. * - * @throws java.util.NoSuchElementException if the requested image does not exists. * @return the image corresponding to the given id + * @throws java.util.NoSuchElementException if the requested image does not exists */ public Image image(int id) { - if (this.imageOrNull(id) == null) - throw new NoSuchElementException(); - else - return this.imageOrNull(id); + return this.images.get(id); } /** * Returns the image corresponding to the given id. * - * @return the image corresponding to the given id or null + * @return the image corresponding to the given id or null if not found */ - public Image imageOrNull(int id){ - // Ugly! Probably a better way... - if (mapFilesId().containsKey(id)) - try { - return ImageIO.read(mapFilesId().get(id)); - } catch (java.io.IOException e) { - return null; - } - else + public Image imageOrNull(int id) { + try { + return this.image(id); + } catch (NoSuchElementException e) { return null; + } } + } diff --git a/src/ch/epfl/xblast/client/painter/ScorePainter.java b/src/ch/epfl/xblast/client/painter/ScorePainter.java new file mode 100644 index 0000000..e412dd8 --- /dev/null +++ b/src/ch/epfl/xblast/client/painter/ScorePainter.java @@ -0,0 +1,107 @@ +package ch.epfl.xblast.client.painter; + +import ch.epfl.xblast.PlayerID; +import ch.epfl.xblast.client.GameState; +import ch.epfl.xblast.client.ImageCollection; + +import java.awt.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * The score painter. + * + * @author Pacien TRAN-GIRARD (261948) + * @author Timothée FLOURE (257420) + */ +public class ScorePainter { + + private static final int SCORE_SEPARATOR_SIZE = 8; + private static final int PLAYER_IMG_MULTIPLIER_OFFSET = 2; + private static final int PLAYER_IMG_DEAD_OFFSET = 1; + + private static final Image TEXT_MIDDLE_IMG = ImageCollection.SCORES_IMAGES.imageOrNull(10); + private static final Image TEXT_RIGHT_IMG = ImageCollection.SCORES_IMAGES.imageOrNull(11); + private static final Image VOID_IMG = ImageCollection.SCORES_IMAGES.imageOrNull(12); + + private ScorePainter() { + // Static class + } + + /** + * Builds the score panel for the given players. + * + * @param players the players + * @return the score panel + */ + public static List buildScorePanel(List players) { + if (Objects.isNull(players)) + throw new IllegalArgumentException(); + + return players.stream() + .flatMap(p -> Stream.concat( + buildPlayerScorePanel(p).stream(), + p.id().ordinal() % 2 == 0 ? buildSeparator().stream() : Stream.empty())) + .collect(Collectors.toList()); + } + + /** + * Builds the score panel for the given player. + * + * @param p the player + * @return the score panel + */ + private static List buildPlayerScorePanel(GameState.Player p) { + if (Objects.isNull(p)) + throw new IllegalArgumentException(); + + return Arrays.asList( + ImageCollection.PLAYERS_IMAGES.imageOrNull(imageIDForPlayer(p)), + TEXT_MIDDLE_IMG, + TEXT_RIGHT_IMG); + } + + /** + * Returns the image ID for the given player. + * + * @param p the player + * @return the image ID + */ + private static int imageIDForPlayer(GameState.Player p) { + return imageIDOffsetForPlayerID(p.id()) + imageIDOffsetForLifeState(p.lives()); + } + + /** + * Returns the image ID offset corresponding to a player ID. + * + * @param pid the player ID + * @return the image ID offset + */ + private static int imageIDOffsetForPlayerID(PlayerID pid) { + return pid.ordinal() * PLAYER_IMG_MULTIPLIER_OFFSET; + } + + /** + * Returns the image ID offset corresponding to a life state. + * + * @param lives the number of remaining lives + * @return the image ID offset + */ + private static int imageIDOffsetForLifeState(int lives) { + return lives == 0 ? PLAYER_IMG_DEAD_OFFSET : 0; + } + + /** + * Builds and returns a separator image sequence. + * + * @return the separator + */ + private static List buildSeparator() { + return Collections.nCopies(SCORE_SEPARATOR_SIZE, VOID_IMG); + } + +} diff --git a/src/ch/epfl/xblast/client/painter/TimeLinePainter.java b/src/ch/epfl/xblast/client/painter/TimeLinePainter.java new file mode 100644 index 0000000..7912d88 --- /dev/null +++ b/src/ch/epfl/xblast/client/painter/TimeLinePainter.java @@ -0,0 +1,39 @@ +package ch.epfl.xblast.client.painter; + +import ch.epfl.xblast.Lists; +import ch.epfl.xblast.client.ImageCollection; +import ch.epfl.xblast.server.Ticks; + +import java.awt.*; +import java.util.Collections; +import java.util.List; + +/** + * The time line painter. + * + * @author Pacien TRAN-GIRARD (261948) + */ +public class TimeLinePainter { + + private static final int TIME_LINE_SIZE = Ticks.GAME_DURATION / 2; + + private static final Image LED_OFF_IMG = ImageCollection.TIME_IMAGES.imageOrNull(20); + private static final Image LED_ON_IMG = ImageCollection.TIME_IMAGES.imageOrNull(21); + + private TimeLinePainter() { + // Static class + } + + /** + * Builds the time line image list + * + * @param t the remaining time + * @return the list of images composing the time "line" + */ + public static List buildTimeLine(int t) { + return Lists.concatenated( + Collections.nCopies(t, LED_ON_IMG), + Collections.nCopies(TIME_LINE_SIZE - t, LED_OFF_IMG)); + } + +} -- cgit v1.2.3