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-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org