From 48d8fc2efc91f5a02391fb1031e7a662509f5630 Mon Sep 17 00:00:00 2001 From: Pacien TRAN-GIRARD Date: Wed, 11 May 2016 22:45:02 +0200 Subject: Implement client --- src/ch/epfl/xblast/Time.java | 13 ++ src/ch/epfl/xblast/client/Client.java | 188 ++++++++++++++++++++- .../epfl/xblast/client/KeyboardEventHandler.java | 29 ++++ src/ch/epfl/xblast/server/GameState.java | 1 + src/ch/epfl/xblast/server/Server.java | 11 +- 5 files changed, 228 insertions(+), 14 deletions(-) (limited to 'src/ch/epfl') diff --git a/src/ch/epfl/xblast/Time.java b/src/ch/epfl/xblast/Time.java index 7c84257..c53f1ef 100644 --- a/src/ch/epfl/xblast/Time.java +++ b/src/ch/epfl/xblast/Time.java @@ -28,4 +28,17 @@ public interface Time { */ int NS_PER_S = 1000 * US_PER_S; + /** + * Pauses the current thread for the given duration. + * + * @param ns the sleep duration (ns) + */ + static void sleep(long ns) { + try { + Thread.sleep((long) (ns / 1E6), (int) (ns % 1E6)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } diff --git a/src/ch/epfl/xblast/client/Client.java b/src/ch/epfl/xblast/client/Client.java index dc08f1b..5b331f2 100644 --- a/src/ch/epfl/xblast/client/Client.java +++ b/src/ch/epfl/xblast/client/Client.java @@ -1,9 +1,25 @@ package ch.epfl.xblast.client; +import ch.epfl.xblast.Lists; +import ch.epfl.xblast.PlayerAction; +import ch.epfl.xblast.PlayerID; +import ch.epfl.xblast.Time; import ch.epfl.xblast.server.Server; +import javax.swing.*; +import java.awt.*; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.StandardProtocolFamily; +import java.nio.ByteBuffer; +import java.nio.channels.DatagramChannel; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Optional; +import java.util.function.Consumer; /** * The Client class. @@ -14,17 +30,179 @@ public class Client { private static final String DEFAULT_SERVER_HOST = "localhost"; private static final int DEFAULT_SERVER_PORT = Server.DEFAULT_PORT; + private static final int PACKET_MAX_SIZE = 1000; + private static final long REGISTER_PING_INTERVAL = 1 * Time.NS_PER_S; // ns - private final InetSocketAddress serverAddr; + private static class Channel { + + private static List bufferToList(ByteBuffer buf) { + List l = new ArrayList<>(buf.remaining()); + + while (buf.hasRemaining()) + l.add(buf.get()); + + return Collections.unmodifiableList(l); + } + + private static DatagramChannel openChannel() { + try { + return DatagramChannel.open(StandardProtocolFamily.INET); + } catch (IOException e) { + e.printStackTrace(); + System.exit(1); + return null; + } + } + + private final SocketAddress serverAddr; + private final DatagramChannel channel; + + Channel(InetSocketAddress iface) { + this.serverAddr = iface; + this.channel = openChannel(); + } + + Channel(String host, Integer port) { + this(new InetSocketAddress( + Optional.ofNullable(host).orElse(DEFAULT_SERVER_HOST), + Optional.ofNullable(port).orElse(DEFAULT_SERVER_PORT))); + } + + void closeChannel() { + try { + this.channel.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + void sendAction(PlayerAction action) { + this.sendByte(action.toByte()); + } + + List receiveGameState(boolean block) { + Optional> state; + + do { + state = this.receive(block); + } while (!state.isPresent()); + + return state.get(); + } + + private void sendByte(byte b) { + this.send(ByteBuffer.wrap(new byte[]{b})); + } + + private void send(ByteBuffer b) { + try { + this.channel.send(b, this.serverAddr); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private Optional> receive(boolean block) { + try { + ByteBuffer buf = ByteBuffer.allocate(PACKET_MAX_SIZE); + this.channel.configureBlocking(block); + this.channel.receive(buf); + buf.flip(); + return Optional.of(bufferToList(buf)); + } catch (IOException e) { + e.printStackTrace(); + return Optional.empty(); + } + } + + } + + private static class GUI { + + private static JFrame buildFrame(Container content) { + JFrame frame = new JFrame(); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setVisible(true); + + frame.setContentPane(content); + frame.pack(); + return frame; + } + + private static void attachKeyboardHandler(Component comp, Consumer actionConsumer) { + comp.addKeyListener(new KeyboardEventHandler(actionConsumer)); + comp.requestFocusInWindow(); + } + + private final XBlastComponent gameComponent; + private final Consumer actionConsumer; + + GUI(Consumer actionConsumer) { + this.gameComponent = new XBlastComponent(); + this.actionConsumer = actionConsumer; + } + + public void display() { + buildFrame(this.gameComponent); + attachKeyboardHandler(this.gameComponent, this.actionConsumer); + } + + public void setGameState(GameState gs, PlayerID p) { + this.gameComponent.setGameState(gs, p); + } + + } + + private final Channel channel; + private final GUI gui; public Client(String host, Integer port) { - this.serverAddr = new InetSocketAddress( - Optional.ofNullable(host).orElse(DEFAULT_SERVER_HOST), - Optional.ofNullable(port).orElse(DEFAULT_SERVER_PORT)); + this.channel = new Channel(host, port); + this.gui = new GUI(this.channel::sendAction); } public void run() { - // TODO + this.displayGUI(); + this.establishConnection(); + this.runGame(); + this.channel.closeChannel(); + } + + private void displayGUI() { + try { + SwingUtilities.invokeAndWait(this.gui::display); + } catch (InterruptedException | InvocationTargetException e) { + e.printStackTrace(); + System.exit(1); + } + } + + private void runGame() { + while (true) + updateGameState(); + } + + private void establishConnection() { + Thread joinThread = new Thread(this::sendJoinRequest); + joinThread.start(); + updateGameState(); + joinThread.interrupt(); + } + + private void updateGameState() { + List serializedGameState = this.channel.receiveGameState(true); + if (serializedGameState.size() < 1) return; + PlayerID player = PlayerID.fromByte(serializedGameState.get(0)); + GameState gameState = GameStateDeserializer.deserialize(Lists.firstDropped(serializedGameState, 1)); + this.gui.setGameState(gameState, player); + } + + private void sendJoinRequest() { + while (!Thread.currentThread().isInterrupted()) { + this.channel.sendAction(PlayerAction.JOIN_GAME); + Time.sleep(REGISTER_PING_INTERVAL); + } } } diff --git a/src/ch/epfl/xblast/client/KeyboardEventHandler.java b/src/ch/epfl/xblast/client/KeyboardEventHandler.java index 2060959..ab64c80 100644 --- a/src/ch/epfl/xblast/client/KeyboardEventHandler.java +++ b/src/ch/epfl/xblast/client/KeyboardEventHandler.java @@ -5,6 +5,8 @@ import ch.epfl.xblast.PlayerAction; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; +import java.util.Collections; +import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.function.Consumer; @@ -17,6 +19,24 @@ import java.util.function.Consumer; */ public class KeyboardEventHandler extends KeyAdapter { + /** + * The default key->action mapping. + */ + private static final Map DEFAULT_KEY_MAP = buildDefaultKeyMap(); + + private static Map buildDefaultKeyMap() { + Map playerActionMap = new HashMap<>(); + + playerActionMap.put(KeyEvent.VK_UP, PlayerAction.MOVE_N); + playerActionMap.put(KeyEvent.VK_RIGHT, PlayerAction.MOVE_E); + playerActionMap.put(KeyEvent.VK_DOWN, PlayerAction.MOVE_S); + playerActionMap.put(KeyEvent.VK_LEFT, PlayerAction.MOVE_W); + playerActionMap.put(KeyEvent.VK_SPACE, PlayerAction.DROP_BOMB); + playerActionMap.put(KeyEvent.VK_SHIFT, PlayerAction.STOP); + + return Collections.unmodifiableMap(playerActionMap); + } + private final Map playerActionMap; private final Consumer consumer; @@ -31,6 +51,15 @@ public class KeyboardEventHandler extends KeyAdapter { this.consumer = consumer; } + /** + * Instantiates a new KeyboardEventHandler with the default key mapping. + * + * @param consumer consumer related to the EventHandler + */ + public KeyboardEventHandler(Consumer consumer) { + this(DEFAULT_KEY_MAP, consumer); + } + /** * Executes the command related to the given key code. * diff --git a/src/ch/epfl/xblast/server/GameState.java b/src/ch/epfl/xblast/server/GameState.java index 2aea7f2..a5e1378 100644 --- a/src/ch/epfl/xblast/server/GameState.java +++ b/src/ch/epfl/xblast/server/GameState.java @@ -215,6 +215,7 @@ public final class GameState { */ public GameState next(Map playerActions) { Map> speedChangeEvents = playerActions.entrySet().stream() + .filter(e -> Objects.nonNull(e.getValue().associatedSpeedChangeEvent())) .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().associatedSpeedChangeEvent())); Set bombDropEvents = playerActions.entrySet().stream() diff --git a/src/ch/epfl/xblast/server/Server.java b/src/ch/epfl/xblast/server/Server.java index 4929dd9..1f29607 100644 --- a/src/ch/epfl/xblast/server/Server.java +++ b/src/ch/epfl/xblast/server/Server.java @@ -3,6 +3,7 @@ package ch.epfl.xblast.server; import ch.epfl.xblast.Lists; import ch.epfl.xblast.PlayerAction; import ch.epfl.xblast.PlayerID; +import ch.epfl.xblast.Time; import ch.epfl.xblast.server.painter.BoardPainter; import java.io.IOException; @@ -154,14 +155,6 @@ public class Server { } - private static void delay(long ns) { - try { - Thread.sleep((long) (ns / 1E6), (int) (ns % 1E6)); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - private final Channel channel; private final int expectedClients; @@ -192,7 +185,7 @@ public class Server { long computationStartTime = System.nanoTime(); gameState = updateGameState(updateGameState(gameState)); broadcastGameState(gameState); - delay(REFRESH_RATE - (computationStartTime - System.nanoTime())); + Time.sleep(REFRESH_RATE - (computationStartTime - System.nanoTime())); } } -- cgit v1.2.3