Index: webapps/examples/WEB-INF/web.xml IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>ISO-8859-1 =================================================================== --- webapps/examples/WEB-INF/web.xml (revision 1296399) +++ webapps/examples/WEB-INF/web.xml (revision ) @@ -374,8 +374,16 @@ --> - wsEchoMessage - /websocket/echoMessage + wsEchoMessage + /websocket/echoMessage + + wsSnake + websocket.snake.SnakeWebSocketServlet + + + wsSnake + /websocket/snake + Index: webapps/examples/WEB-INF/classes/websocket/snake/SnakeWebSocketServlet.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- webapps/examples/WEB-INF/classes/websocket/snake/SnakeWebSocketServlet.java (revision ) +++ webapps/examples/WEB-INF/classes/websocket/snake/SnakeWebSocketServlet.java (revision ) @@ -0,0 +1,192 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package websocket.snake; + +import org.apache.catalina.websocket.MessageInbound; +import org.apache.catalina.websocket.StreamInbound; +import org.apache.catalina.websocket.WebSocketServlet; +import org.apache.catalina.websocket.WsOutbound; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +import javax.servlet.ServletException; +import java.awt.*; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Example web socket servlet for simple multiplayer snake. + * + * @author Johno Crawford (johno@hellface.com) + */ +public class SnakeWebSocketServlet extends WebSocketServlet { + + private static final long serialVersionUID = 1L; + + private static final Log log = LogFactory.getLog(SnakeWebSocketServlet.class); + + public static final int PLAYFIELD_WIDTH = 640; + public static final int PLAYFIELD_HEIGHT = 480; + public static final int GRID_SIZE = 10; + + private static final long TICK_DELAY = 100; + + private static final Random random = new Random(); + + private final Timer gameTimer = new Timer(SnakeWebSocketServlet.class.getSimpleName() + " Timer"); + + private final AtomicInteger connectionIds = new AtomicInteger(0); + private final ConcurrentHashMap snakes = new ConcurrentHashMap(); + private final ConcurrentHashMap connections = new ConcurrentHashMap(); + + @Override + public void init() throws ServletException { + super.init(); + gameTimer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + try { + tick(); + } catch (RuntimeException e) { + log.error("Caught runtime to prevent timer from shutting down", e); + } + } + }, TICK_DELAY, TICK_DELAY); + } + + private void tick() { + StringBuilder sb = new StringBuilder(); + for (Iterator iterator = getSnakes().iterator(); iterator.hasNext(); ) { + Snake snake = iterator.next(); + snake.update(getSnakes()); + sb.append(snake.getLocationsJson()); + if (iterator.hasNext()) { + sb.append(','); + } + } + broadcast(String.format("{'type': 'update', 'data' : [%s]}", sb.toString())); + } + + private void broadcast(String message) { + for (SnakeMessageInbound connection : getConnections()) { + try { + CharBuffer response = CharBuffer.wrap(message); + connection.getWsOutbound().writeTextMessage(response); + } catch (IOException ignore) { + } + } + } + + private Collection getConnections() { + return Collections.unmodifiableCollection(connections.values()); + } + + private Collection getSnakes() { + return Collections.unmodifiableCollection(snakes.values()); + } + + public static String getRandomHexColor() { + float hue = random.nextFloat(); + float saturation = (random.nextInt(2000) + 1000) / 10000f; // sat between 0.1 and 0.3 + float luminance = 0.9f; + Color color = Color.getHSBColor(hue, saturation, luminance); + return '#' + Integer.toHexString((color.getRGB() & 0xffffff) | 0x1000000).substring(1); + } + + public static Location getRandomLocation() { + int x = roundByGridSize(random.nextInt(SnakeWebSocketServlet.PLAYFIELD_WIDTH)); + int y = roundByGridSize(random.nextInt(SnakeWebSocketServlet.PLAYFIELD_HEIGHT)); + return new Location(x, y); + } + + private static int roundByGridSize(int value) { + value = value + (SnakeWebSocketServlet.GRID_SIZE / 2); + value = value / SnakeWebSocketServlet.GRID_SIZE; + value = value * SnakeWebSocketServlet.GRID_SIZE; + return value; + } + + @Override + public void destroy() { + super.destroy(); + if (gameTimer != null) { + gameTimer.cancel(); + } + } + + @Override + protected StreamInbound createWebSocketInbound(String subProtocol) { + return new SnakeMessageInbound(connectionIds.incrementAndGet()); + } + + private final class SnakeMessageInbound extends MessageInbound { + + private final int id; + private Snake snake; + + private SnakeMessageInbound(int id) { + this.id = id; + } + + @Override + protected void onOpen(WsOutbound outbound) { + this.snake = new Snake(id, outbound); + snakes.put(id, snake); + connections.put(id, this); + StringBuilder sb = new StringBuilder(); + for (Iterator iterator = getSnakes().iterator(); iterator.hasNext(); ) { + Snake snake = iterator.next(); + sb.append(String.format("{id: %d, color: '%s'}", snake.getId(), snake.getHexColor())); + if (iterator.hasNext()) { + sb.append(','); + } + } + broadcast(String.format("{'type': 'join','data':[%s]}", sb.toString())); + } + + @Override + protected void onClose(int status) { + connections.remove(id); + snakes.remove(id); + broadcast(String.format("{'type': 'leave', 'id': %d}", id)); + } + + @Override + protected void onBinaryMessage(ByteBuffer message) throws IOException { + throw new UnsupportedOperationException("Binary message not supported."); + } + + @Override + protected void onTextMessage(CharBuffer charBuffer) throws IOException { + String message = charBuffer.toString(); + if ("left".equals(message)) { + snake.setDirection(Direction.WEST); + } else if ("up".equals(message)) { + snake.setDirection(Direction.NORTH); + } else if ("right".equals(message)) { + snake.setDirection(Direction.EAST); + } else if ("down".equals(message)) { + snake.setDirection(Direction.SOUTH); + } + } + } + +} Index: webapps/examples/WEB-INF/classes/websocket/snake/Direction.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- webapps/examples/WEB-INF/classes/websocket/snake/Direction.java (revision ) +++ webapps/examples/WEB-INF/classes/websocket/snake/Direction.java (revision ) @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package websocket.snake; + +/** + * @author Johno Crawford (johno@hellface.com) + */ +public enum Direction { + NONE, NORTH, SOUTH, EAST, WEST +} Index: webapps/examples/WEB-INF/classes/websocket/snake/Snake.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- webapps/examples/WEB-INF/classes/websocket/snake/Snake.java (revision ) +++ webapps/examples/WEB-INF/classes/websocket/snake/Snake.java (revision ) @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package websocket.snake; + +import org.apache.catalina.websocket.WsOutbound; + +import java.io.IOException; +import java.nio.CharBuffer; +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.Deque; +import java.util.Iterator; + +/** + * @author Johno Crawford (johno@hellface.com) + */ +public class Snake { + + private static final int DEFAULT_LENGTH = 6; + + private final int id; + private final WsOutbound outbound; + + private Direction direction; + private Deque locations = new ArrayDeque(); + private String hexColor; + + public Snake(int id, WsOutbound outbound) { + this.id = id; + this.outbound = outbound; + this.hexColor = SnakeWebSocketServlet.getRandomHexColor(); + resetState(); + } + + private void resetState() { + this.direction = Direction.NONE; + this.locations.clear(); + Location startLocation = SnakeWebSocketServlet.getRandomLocation(); + for (int i = 0; i < DEFAULT_LENGTH; i++) { + locations.add(startLocation); + } + } + + private void kill() { + resetState(); + try { + CharBuffer response = CharBuffer.wrap("{'type': 'dead'}"); + outbound.writeTextMessage(response); + } catch (IOException ignore) { + } + } + + public synchronized void update(Collection snakes) { + Location firstLocation = locations.getFirst(); + Location nextLocation = firstLocation.getAdjacentLocation(direction); + if (nextLocation.x >= SnakeWebSocketServlet.PLAYFIELD_WIDTH) { + nextLocation.x = 0; + } + if (nextLocation.y >= SnakeWebSocketServlet.PLAYFIELD_HEIGHT) { + nextLocation.y = 0; + } + if (nextLocation.x < 0) { + nextLocation.x = SnakeWebSocketServlet.PLAYFIELD_WIDTH; + } + if (nextLocation.y < 0) { + nextLocation.y = SnakeWebSocketServlet.PLAYFIELD_HEIGHT; + } + locations.addFirst(nextLocation); + locations.removeLast(); + + for (Snake snake : snakes) { + if (snake.getId() != getId() && colliding(snake.getHeadLocation())) { + snake.kill(); + } + } + } + + private boolean colliding(Location location) { + return direction != Direction.NONE && locations.contains(location); + } + + public void setDirection(Direction direction) { + this.direction = direction; + } + + public synchronized String getLocationsJson() { + StringBuilder sb = new StringBuilder(); + for (Iterator iterator = locations.iterator(); iterator.hasNext(); ) { + Location location = iterator.next(); + sb.append(String.format("{x: %d, y: %d}", location.x, location.y)); + if (iterator.hasNext()) { + sb.append(','); + } + } + return String.format("{'id':%d,'body':[%s]}", id, sb.toString()); + } + + public int getId() { + return id; + } + + public String getHexColor() { + return hexColor; + } + + public synchronized Location getHeadLocation() { + return locations.getFirst(); + } +} Index: webapps/examples/WEB-INF/classes/websocket/snake/Location.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- webapps/examples/WEB-INF/classes/websocket/snake/Location.java (revision ) +++ webapps/examples/WEB-INF/classes/websocket/snake/Location.java (revision ) @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package websocket.snake; + +/** + * @author Johno Crawford (johno@hellface.com) + */ +public class Location { + + public int x; + public int y; + + public Location(int x, int y) { + this.x = x; + this.y = y; + } + + public Location getAdjacentLocation(Direction direction) { + switch (direction) { + case NORTH: + return new Location(x, y - SnakeWebSocketServlet.GRID_SIZE); + case SOUTH: + return new Location(x, y + SnakeWebSocketServlet.GRID_SIZE); + case EAST: + return new Location(x + SnakeWebSocketServlet.GRID_SIZE, y); + case WEST: + return new Location(x - SnakeWebSocketServlet.GRID_SIZE, y); + case NONE: + // fall through + default: + return this; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Location location = (Location) o; + + if (x != location.x) return false; + if (y != location.y) return false; + + return true; + } + + @Override + public int hashCode() { + int result = x; + result = 31 * result + y; + return result; + } +} Index: webapps/examples/websocket/snake.html IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- webapps/examples/websocket/snake.html (revision ) +++ webapps/examples/websocket/snake.html (revision ) @@ -0,0 +1,241 @@ + + + + + + Apache Tomcat WebSocket Examples: Multiplayer Snake + + + + +
+ +
+
+
+
+ + + \ No newline at end of file Index: webapps/examples/websocket/echo.html IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- webapps/examples/websocket/echo.html (revision 1296399) +++ webapps/examples/websocket/echo.html (revision ) @@ -31,7 +31,6 @@ #console-container { float: left; margin-left: 20px; - padding-left: 20px; width: 400px; } \ No newline at end of file Index: webapps/examples/websocket/index.html IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- webapps/examples/websocket/index.html (revision 1296399) +++ webapps/examples/websocket/index.html (revision ) @@ -23,6 +23,7 @@

Apache Tomcat WebSocket Examples

\ No newline at end of file