You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by ma...@apache.org on 2012/03/06 16:14:50 UTC

svn commit: r1297520 - in /tomcat/trunk/webapps/examples: WEB-INF/ WEB-INF/classes/websocket/snake/ websocket/

Author: markt
Date: Tue Mar  6 15:14:50 2012
New Revision: 1297520

URL: http://svn.apache.org/viewvc?rev=1297520&view=rev
Log:
Add a multi-player snake example for WebSocket.
Patch provide by Johno Crawford.

Added:
    tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/snake/
    tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/snake/Direction.java
    tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/snake/Location.java
    tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/snake/Snake.java
    tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/snake/SnakeWebSocketServlet.java
    tomcat/trunk/webapps/examples/websocket/snake.html
Modified:
    tomcat/trunk/webapps/examples/WEB-INF/web.xml
    tomcat/trunk/webapps/examples/websocket/echo.html
    tomcat/trunk/webapps/examples/websocket/index.html

Added: tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/snake/Direction.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/snake/Direction.java?rev=1297520&view=auto
==============================================================================
--- tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/snake/Direction.java (added)
+++ tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/snake/Direction.java Tue Mar  6 15:14:50 2012
@@ -0,0 +1,21 @@
+/*
+ * 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;
+
+public enum Direction {
+    NONE, NORTH, SOUTH, EAST, WEST
+}

Added: tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/snake/Location.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/snake/Location.java?rev=1297520&view=auto
==============================================================================
--- tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/snake/Location.java (added)
+++ tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/snake/Location.java Tue Mar  6 15:14:50 2012
@@ -0,0 +1,65 @@
+/*
+ * 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;
+
+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;
+    }
+}

Added: tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/snake/Snake.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/snake/Snake.java?rev=1297520&view=auto
==============================================================================
--- tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/snake/Snake.java (added)
+++ tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/snake/Snake.java Tue Mar  6 15:14:50 2012
@@ -0,0 +1,142 @@
+/*
+ * 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 java.io.IOException;
+import java.nio.CharBuffer;
+import java.util.ArrayDeque;
+import java.util.Collection;
+import java.util.Deque;
+import java.util.Iterator;
+
+import org.apache.catalina.websocket.WsOutbound;
+
+public class Snake {
+
+    private static final int DEFAULT_LENGTH = 6;
+
+    private final int id;
+    private final WsOutbound outbound;
+
+    private Direction direction;
+    private Deque<Location> locations = new ArrayDeque<Location>();
+    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 ioe) {
+            // Ignore
+        }
+    }
+
+    private void reward() {
+        grow();
+        try {
+            CharBuffer response = CharBuffer.wrap("{'type': 'kill'}");
+            outbound.writeTextMessage(response);
+        } catch (IOException ioe) {
+            // Ignore
+        }
+    }
+
+    public synchronized void update(Collection<Snake> 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();
+                reward();
+            }
+        }
+    }
+
+    private void grow() {
+        Location lastLocation = locations.getLast();
+        Location newLocation = new Location(lastLocation.x, lastLocation.y);
+        locations.add(newLocation);
+    }
+
+    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<Location> iterator = locations.iterator();
+                iterator.hasNext();) {
+            Location location = iterator.next();
+            sb.append(String.format("{x: %d, y: %d}",
+                    Integer.valueOf(location.x), Integer.valueOf(location.y)));
+            if (iterator.hasNext()) {
+                sb.append(',');
+            }
+        }
+        return String.format("{'id':%d,'body':[%s]}",
+                Integer.valueOf(id), sb.toString());
+    }
+
+    public int getId() {
+        return id;
+    }
+
+    public String getHexColor() {
+        return hexColor;
+    }
+
+    public synchronized Location getHeadLocation() {
+        return locations.getFirst();
+    }
+}

Added: tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/snake/SnakeWebSocketServlet.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/snake/SnakeWebSocketServlet.java?rev=1297520&view=auto
==============================================================================
--- tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/snake/SnakeWebSocketServlet.java (added)
+++ tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/snake/SnakeWebSocketServlet.java Tue Mar  6 15:14:50 2012
@@ -0,0 +1,210 @@
+/*
+ * 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 java.awt.Color;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Random;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.servlet.ServletException;
+
+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;
+
+/**
+ * Example web socket servlet for simple multiplayer snake.
+ */
+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<Integer, Snake> snakes =
+            new ConcurrentHashMap<Integer, Snake>();
+    private final ConcurrentHashMap<Integer, SnakeMessageInbound> connections =
+            new ConcurrentHashMap<Integer, SnakeMessageInbound>();
+
+    @Override
+    public void init() throws ServletException {
+        super.init();
+        gameTimer.scheduleAtFixedRate(new TimerTask() {
+            @Override
+            public void run() {
+                try {
+                    tick();
+                } catch (RuntimeException e) {
+                    log.error("Caught to prevent timer from shutting down", e);
+                }
+            }
+        }, TICK_DELAY, TICK_DELAY);
+    }
+
+    private void tick() {
+        StringBuilder sb = new StringBuilder();
+        for (Iterator<Snake> 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<SnakeMessageInbound> getConnections() {
+        return Collections.unmodifiableCollection(connections.values());
+    }
+
+    private Collection<Snake> getSnakes() {
+        return Collections.unmodifiableCollection(snakes.values());
+    }
+
+    public static String getRandomHexColor() {
+        float hue = random.nextFloat();
+        // sat between 0.1 and 0.3
+        float saturation = (random.nextInt(2000) + 1000) / 10000f;
+        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(Integer.valueOf(id), snake);
+            connections.put(Integer.valueOf(id), this);
+            StringBuilder sb = new StringBuilder();
+            for (Iterator<Snake> iterator = getSnakes().iterator();
+                    iterator.hasNext();) {
+                Snake snake = iterator.next();
+                sb.append(String.format("{id: %d, color: '%s'}",
+                        Integer.valueOf(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(Integer.valueOf(id));
+            snakes.remove(Integer.valueOf(id));
+            broadcast(String.format("{'type': 'leave', 'id': %d}",
+                    Integer.valueOf(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);
+            }
+        }
+    }
+}

Modified: tomcat/trunk/webapps/examples/WEB-INF/web.xml
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/WEB-INF/web.xml?rev=1297520&r1=1297519&r2=1297520&view=diff
==============================================================================
--- tomcat/trunk/webapps/examples/WEB-INF/web.xml (original)
+++ tomcat/trunk/webapps/examples/WEB-INF/web.xml Tue Mar  6 15:14:50 2012
@@ -377,5 +377,13 @@
       <servlet-name>wsEchoMessage</servlet-name>
       <url-pattern>/websocket/echoMessage</url-pattern>
     </servlet-mapping>
+    <servlet>
+      <servlet-name>wsSnake</servlet-name>
+      <servlet-class>websocket.snake.SnakeWebSocketServlet</servlet-class>
+    </servlet>
+    <servlet-mapping>
+      <servlet-name>wsSnake</servlet-name>
+      <url-pattern>/websocket/snake</url-pattern>
+    </servlet-mapping>
 
 </web-app>

Modified: tomcat/trunk/webapps/examples/websocket/echo.html
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/websocket/echo.html?rev=1297520&r1=1297519&r2=1297520&view=diff
==============================================================================
--- tomcat/trunk/webapps/examples/websocket/echo.html (original)
+++ tomcat/trunk/webapps/examples/websocket/echo.html Tue Mar  6 15:14:50 2012
@@ -30,7 +30,6 @@
 
         #console-container {
             float: left;
-            margin-left: 20px;
             padding-left: 20px;
             width: 400px;
         }

Modified: tomcat/trunk/webapps/examples/websocket/index.html
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/websocket/index.html?rev=1297520&r1=1297519&r2=1297520&view=diff
==============================================================================
--- tomcat/trunk/webapps/examples/websocket/index.html (original)
+++ tomcat/trunk/webapps/examples/websocket/index.html Tue Mar  6 15:14:50 2012
@@ -24,5 +24,6 @@
 <P></P>
 <ul>
 <li><a href="echo.html">Echo example</a></li>
+<li><a href="snake.html">Multiplayer snake example</a></li>
 </ul>
 </BODY></HTML>
\ No newline at end of file

Added: tomcat/trunk/webapps/examples/websocket/snake.html
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/websocket/snake.html?rev=1297520&view=auto
==============================================================================
--- tomcat/trunk/webapps/examples/websocket/snake.html (added)
+++ tomcat/trunk/webapps/examples/websocket/snake.html Tue Mar  6 15:14:50 2012
@@ -0,0 +1,244 @@
+<!--
+  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.
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+    <title>Apache Tomcat WebSocket Examples: Multiplayer Snake</title>
+    <style type="text/css">
+        #playground {
+            width: 640px;
+            height: 480px;
+            background-color: #000;
+        }
+
+        #console-container {
+            float: left;
+            margin-left: 15px;
+            width: 300px;
+        }
+
+        #console {
+            border: 1px solid #CCCCCC;
+            border-right-color: #999999;
+            border-bottom-color: #999999;
+            height: 480px;
+            overflow-y: scroll;
+            padding-left: 5px;
+            padding-right: 5px;
+            width: 100%;
+        }
+
+        #console p {
+            padding: 0;
+            margin: 0;
+        }
+    </style>
+</head>
+<body>
+    <noscript><h1>Seems your browser doesn't support Javascript! Websockets rely on Javascript being enabled. Please enable
+    Javascript and reload this page!</h1></noscript>
+    <div style="float: left">
+        <canvas id="playground" width="640" height="480"></canvas>
+    </div>
+    <div id="console-container">
+        <div id="console"></div>
+    </div>
+    <script type="text/javascript">
+
+        var Game = {};
+
+        Game.fps = 30;
+        Game.socket = null;
+        Game.nextFrame = null;
+        Game.interval = null;
+        Game.gridSize = 10;
+
+        function Snake() {
+            this.snakeBody = [];
+            this.color = null;
+        }
+
+        Snake.prototype.draw = function(context) {
+            for (var id in this.snakeBody) {
+                context.fillStyle = this.color;
+                context.fillRect(this.snakeBody[id].x, this.snakeBody[id].y, Game.gridSize, Game.gridSize);
+            }
+        };
+
+        Game.initialize = function() {
+            this.entities = [];
+            canvas = document.getElementById('playground');
+            if (!canvas.getContext) {
+                Console.log('Error: 2d canvas not supported by this browser.');
+                return;
+            }
+            this.context = canvas.getContext('2d');
+            window.addEventListener('keydown', function (e) {
+                var code = e.keyCode;
+                if (code > 36 && code < 41) {
+                    switch (code) {
+                        case 37:
+                            Game.socket.send('left');
+                            break;
+                        case 38:
+                            Game.socket.send('up');
+                            break;
+                        case 39:
+                            Game.socket.send('right');
+                            break;
+                        case 40:
+                            Game.socket.send('down');
+                            break;
+                    }
+                    Console.log('Sent: keyCode ' + code);
+                }
+            }, false);
+            Game.connect('ws://' + window.location.host + '/examples/websocket/snake');
+        };
+
+        Game.startGameLoop = function() {
+            if (window.webkitRequestAnimationFrame) {
+                Game.nextFrame = function () {
+                    webkitRequestAnimationFrame(Game.run);
+                };
+            } else if (window.mozRequestAnimationFrame) {
+                Game.nextFrame = function () {
+                    mozRequestAnimationFrame(Game.run);
+                };
+            } else {
+                Game.interval = setInterval(Game.run, 1000 / Game.fps);
+            }
+            if (Game.nextFrame != null) {
+                Game.nextFrame();
+            }
+        };
+
+        Game.stopGameLoop = function () {
+            Game.nextFrame = null;
+            if (Game.interval != null) {
+                clearInterval(Game.interval);
+            }
+        };
+
+        Game.draw = function() {
+            this.context.clearRect(0, 0, 640, 480);
+            for (var id in this.entities) {
+                this.entities[id].draw(this.context);
+            }
+        };
+
+        Game.addSnake = function(id, color) {
+            Game.entities[id] = new Snake();
+            Game.entities[id].color = color;
+        };
+
+        Game.updateSnake = function(id, snakeBody) {
+            if (typeof Game.entities[id] != "undefined") {
+                Game.entities[id].snakeBody = snakeBody;
+            }
+        };
+
+        Game.removeSnake = function(id) {
+            Game.entities[id] = null;
+            delete Game.entities[id]; // force GC
+        };
+
+        Game.run = (function() {
+            var skipTicks = 1000 / Game.fps, nextGameTick = (new Date).getTime();
+
+            return function() {
+                while ((new Date).getTime() > nextGameTick) {
+                    nextGameTick += skipTicks;
+                }
+                Game.draw();
+                if (Game.nextFrame != null) {
+                    Game.nextFrame();
+                }
+            };
+        })();
+
+        Game.connect = (function(host) {
+            if ('WebSocket' in window) {
+                Game.socket = new WebSocket(host);
+            } else if ('MozWebSocket' in window) {
+                Game.socket = new MozWebSocket(host);
+            } else {
+                Console.log('Error: WebSocket is not supported by this browser.');
+                return;
+            }
+
+            Game.socket.onopen = function () {
+                // Socket initialised.. start the game loop
+                Console.log('Info: WebSocket connection opened.');
+                Console.log('Info: Press an arrow key to begin.');
+                Game.startGameLoop();
+                setInterval(function() {
+                    Game.socket.send('ping'); // prevent server read timeout
+                }, 5000);
+            };
+
+            Game.socket.onclose = function () {
+                Console.log('Info: WebSocket closed.');
+                Game.stopGameLoop();
+            };
+
+            Game.socket.onmessage = function (message) {
+                var packet = eval('(' + message.data + ')'); // Consider using json lib to parse data.
+                switch (packet.type) {
+                    case 'update':
+                        for (var i = 0; i < packet.data.length; i++) {
+                            Game.updateSnake(packet.data[i].id, packet.data[i].body);
+                        }
+                        break;
+                    case 'join':
+                        for (var j = 0; j < packet.data.length; j++) {
+                            Game.addSnake(packet.data[j].id, packet.data[j].color);
+                        }
+                        break;
+                    case 'leave':
+                        Game.removeSnake(packet.id);
+                        break;
+                    case 'dead':
+                        Console.log('Info: Your snake is dead, bad luck!');
+                        break;
+                    case 'kill':
+                        Console.log('Info: Head shot!');
+                        break;
+                }
+            };
+        });
+
+        var Console = {};
+
+        Console.log = (function(message) {
+            var console = document.getElementById('console');
+            var p = document.createElement('p');
+            p.style.wordWrap = 'break-word';
+            p.innerHTML = message;
+            console.appendChild(p);
+            while (console.childNodes.length > 25) {
+                console.removeChild(console.firstChild);
+            }
+            console.scrollTop = console.scrollHeight;
+        });
+
+        Game.initialize();
+    </script>
+</body>
+</html>



---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org