├── settings.gradle ├── Info └── 2048.gif ├── src ├── main │ ├── resources │ │ ├── application.properties │ │ └── static │ │ │ ├── image │ │ │ ├── favicon.ico │ │ │ ├── apple-touch-icon.png │ │ │ ├── apple-touch-startup-image-640x920.png │ │ │ └── apple-touch-startup-image-640x1096.png │ │ │ ├── js │ │ │ ├── application.js │ │ │ ├── bind_polyfill.js │ │ │ ├── tile.js │ │ │ ├── animframe_polyfill.js │ │ │ ├── local_storage_manager.js │ │ │ ├── classlist_polyfill.js │ │ │ ├── grid.js │ │ │ ├── uuid.js │ │ │ ├── html_actuator.js │ │ │ ├── keyboard_input_manager.js │ │ │ ├── net_send_manager.js │ │ │ ├── remote_game_manager.js │ │ │ └── game_manager.js │ │ │ ├── css │ │ │ ├── helpers.scss │ │ │ ├── main.scss │ │ │ └── main.css │ │ │ └── 2048.html │ ├── webapp │ │ └── index.html │ └── java │ │ └── org │ │ └── decaywood │ │ ├── BootPageApplication.java │ │ ├── buffer │ │ ├── handler │ │ │ ├── BufferExceptionHandler.java │ │ │ └── KeyEventSender.java │ │ ├── MainBuffer.java │ │ ├── MultiSendersBuffer.java │ │ └── MessageBuffer.java │ │ ├── web │ │ └── GameController.java │ │ ├── websocket │ │ └── WebSocketConfig.java │ │ ├── serve │ │ └── ConnectionManager.java │ │ ├── KeyEvent.java │ │ └── KeyEventSequencer.java └── test │ └── java │ └── org │ └── decaywood │ └── BootPageApplicationTests.java ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── README.md ├── gradlew.bat └── gradlew /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'LoftPage' 2 | 3 | -------------------------------------------------------------------------------- /Info/2048.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaywood/LoftPage/HEAD/Info/2048.gif -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=4000 2 | server.context-path=/ 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaywood/LoftPage/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/resources/static/image/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaywood/LoftPage/HEAD/src/main/resources/static/image/favicon.ico -------------------------------------------------------------------------------- /src/main/resources/static/image/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaywood/LoftPage/HEAD/src/main/resources/static/image/apple-touch-icon.png -------------------------------------------------------------------------------- /src/main/resources/static/image/apple-touch-startup-image-640x920.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaywood/LoftPage/HEAD/src/main/resources/static/image/apple-touch-startup-image-640x920.png -------------------------------------------------------------------------------- /src/main/resources/static/image/apple-touch-startup-image-640x1096.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaywood/LoftPage/HEAD/src/main/resources/static/image/apple-touch-startup-image-640x1096.png -------------------------------------------------------------------------------- /src/main/webapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.9-bin.zip 6 | -------------------------------------------------------------------------------- /src/main/resources/static/js/application.js: -------------------------------------------------------------------------------- 1 | // Wait till the browser is ready to render the game (avoids glitches) 2 | window.requestAnimationFrame(function () { 3 | new GameManager(4, KeyboardInputManager, HTMLActuator, LocalStorageManager, NetSendManager, '#home'); 4 | }); 5 | -------------------------------------------------------------------------------- /src/main/resources/static/js/bind_polyfill.js: -------------------------------------------------------------------------------- 1 | Function.prototype.bind = Function.prototype.bind || function (target) { 2 | var self = this; 3 | return function (args) { 4 | if (!(args instanceof Array)) { 5 | args = [args]; 6 | } 7 | self.apply(target, args); 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /src/main/java/org/decaywood/BootPageApplication.java: -------------------------------------------------------------------------------- 1 | package org.decaywood; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class BootPageApplication { 8 | 9 | public static void main(String[] args) throws Exception { 10 | /** 11 | * for debug 12 | */ 13 | System.setProperty("server.port", "4000"); 14 | System.setProperty("server.context-path", "/"); 15 | 16 | 17 | SpringApplication.run(BootPageApplication.class, args); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/org/decaywood/BootPageApplicationTests.java: -------------------------------------------------------------------------------- 1 | package org.decaywood; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.SpringApplicationConfiguration; 6 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 7 | import org.springframework.test.context.web.WebAppConfiguration; 8 | 9 | @RunWith(SpringJUnit4ClassRunner.class) 10 | @SpringApplicationConfiguration(classes = BootPageApplication.class) 11 | @WebAppConfiguration 12 | public class BootPageApplicationTests { 13 | 14 | @Test 15 | public void contextLoads() {} 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/resources/static/js/tile.js: -------------------------------------------------------------------------------- 1 | function Tile(position, value) { 2 | this.x = position.x; 3 | this.y = position.y; 4 | this.value = value || 2; 5 | 6 | this.previousPosition = null; 7 | this.mergedFrom = null; // Tracks tiles that merged together 8 | } 9 | 10 | Tile.prototype.savePosition = function () { 11 | this.previousPosition = { x: this.x, y: this.y }; 12 | }; 13 | 14 | Tile.prototype.updatePosition = function (position) { 15 | if(position == undefined) return; 16 | this.x = position.x; 17 | this.y = position.y; 18 | }; 19 | 20 | Tile.prototype.serialize = function () { 21 | return { 22 | position: { 23 | x: this.x, 24 | y: this.y 25 | }, 26 | value: this.value 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /src/main/java/org/decaywood/buffer/handler/BufferExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package org.decaywood.buffer.handler; 2 | 3 | import com.lmax.disruptor.ExceptionHandler; 4 | import org.apache.log4j.Logger; 5 | import org.decaywood.KeyEvent; 6 | 7 | /** 8 | * @author: decaywood 9 | * @date: 2015/6/19 15:33 10 | */ 11 | public class BufferExceptionHandler implements ExceptionHandler { 12 | 13 | Logger logger = Logger.getLogger(this.getClass().getName()); 14 | 15 | @Override 16 | public void handleEventException(Throwable ex, long sequence, KeyEvent event) { 17 | logger.error(ex.getMessage()); 18 | } 19 | 20 | @Override 21 | public void handleOnStartException(Throwable ex) { 22 | logger.error(ex.getMessage()); 23 | } 24 | 25 | @Override 26 | public void handleOnShutdownException(Throwable ex) { 27 | logger.error(ex.getMessage()); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/resources/static/js/animframe_polyfill.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var lastTime = 0; 3 | var vendors = ['webkit', 'moz']; 4 | for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 5 | window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame']; 6 | window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || 7 | window[vendors[x] + 'CancelRequestAnimationFrame']; 8 | } 9 | 10 | if (!window.requestAnimationFrame) { 11 | window.requestAnimationFrame = function (callback) { 12 | var currTime = new Date().getTime(); 13 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 14 | var id = window.setTimeout(function () { 15 | callback(currTime + timeToCall); 16 | }, 17 | timeToCall); 18 | lastTime = currTime + timeToCall; 19 | return id; 20 | }; 21 | } 22 | 23 | if (!window.cancelAnimationFrame) { 24 | window.cancelAnimationFrame = function (id) { 25 | clearTimeout(id); 26 | }; 27 | } 28 | }()); 29 | -------------------------------------------------------------------------------- /src/main/java/org/decaywood/buffer/MainBuffer.java: -------------------------------------------------------------------------------- 1 | package org.decaywood.buffer; 2 | 3 | import com.lmax.disruptor.RingBuffer; 4 | import org.decaywood.KeyEvent; 5 | 6 | /** 7 | * @author: decaywood 8 | * @date: 2015/6/19 10:42 9 | * 10 | * the super class in which the disruptor is embedded and configured 11 | * 12 | * ringBuffer can be init (lazy) via BufferGenerator which is offered by subclass 13 | * 14 | */ 15 | 16 | public abstract class MainBuffer { 17 | 18 | protected RingBuffer ringBuffer; 19 | 20 | public MainBuffer() { 21 | System.out.println("init"); 22 | } 23 | 24 | 25 | public void publishKeyEvent(KeyEvent event) { 26 | System.out.println("publish Event"); 27 | long sequence = this.ringBuffer.next(); 28 | try { 29 | KeyEvent keyEvent = this.ringBuffer.get(sequence); 30 | keyEvent.copyOf(event); 31 | } finally { 32 | this.ringBuffer.publish(sequence); // ensure that event can be published 33 | } 34 | 35 | } 36 | 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/org/decaywood/web/GameController.java: -------------------------------------------------------------------------------- 1 | package org.decaywood.web; 2 | 3 | import org.decaywood.KeyEvent; 4 | import org.decaywood.buffer.MessageBuffer; 5 | import org.decaywood.serve.ConnectionManager; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | import javax.annotation.Resource; 10 | import javax.servlet.http.HttpServletRequest; 11 | 12 | /** 13 | * @author: decaywood 14 | * @date: 2015/6/15 20:22 15 | */ 16 | @RestController 17 | public class GameController { 18 | 19 | @Resource(name = "ConnectionManager") 20 | private ConnectionManager manager; 21 | 22 | @Resource(name = "MessageBuffer") 23 | private MessageBuffer messageBuffer; 24 | 25 | 26 | @RequestMapping(value = "/connectGame") 27 | public String connectGame(HttpServletRequest request, KeyEvent keyEvent) { 28 | String result; 29 | keyEvent.setIPAddress(request.getRemoteAddr()); 30 | result = manager.connect(keyEvent); 31 | System.out.println(result); 32 | return result; 33 | } 34 | 35 | @RequestMapping(value = "/keyDown") 36 | public void keyDown(HttpServletRequest request, KeyEvent event) { 37 | System.out.println("keydown : " + event); 38 | event.setIPAddress(request.getRemoteAddr()); 39 | this.messageBuffer.publishEvent(event); 40 | 41 | } 42 | 43 | 44 | 45 | 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/org/decaywood/websocket/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package org.decaywood.websocket; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 5 | import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer; 6 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 7 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 8 | import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration; 9 | 10 | 11 | 12 | @Configuration 13 | @EnableWebSocketMessageBroker 14 | public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer { 15 | 16 | 17 | 18 | @Override 19 | public void configureMessageBroker(MessageBrokerRegistry config) { 20 | config.enableSimpleBroker("/message"); 21 | config.setApplicationDestinationPrefixes("/game"); 22 | } 23 | 24 | @Override 25 | public void registerStompEndpoints(StompEndpointRegistry registry) { 26 | registry.addEndpoint("/webSocket") 27 | .withSockJS(); 28 | } 29 | 30 | @Override 31 | public void configureWebSocketTransport(WebSocketTransportRegistration registration) { 32 | registration.setMessageSizeLimit(500 * 1024); 33 | registration.setSendBufferSizeLimit(1024 * 1024); 34 | registration.setSendTimeLimit(600000); 35 | } 36 | 37 | 38 | 39 | 40 | } 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This App is extended from [2048](https://github.com/gabrielecirulli/2048), thanks to gabrielecirulli ! 2 | 3 | the Project has been migrated from maven to gradle... and up to now (version 0.0.2), it still has some bugs.. 4 | 5 | changed: removed large amount of redundant code, replaced Spring integration (Spring MVC, Spring, Tomcat) with **SpringBoot** 6 | 7 | ## install 8 | 9 | ### Preparation 10 | 11 | if you have not gradle, don't worry, you can execute the build using one of the following commands from the root of the project: 12 | 13 | ```bash 14 | # on Unix-like platforms such as Linux and Mac OS X 15 | ./gradlew bootRun 16 | 17 | # on Windows using the gradlew.bat batch file 18 | gradlew bootRun 19 | ``` 20 | 21 | ### Build an executable JAR 22 | 23 | You can run the application without building 24 | 25 | ```bash 26 | $ gradle bootRun 27 | ``` 28 | 29 | You might want to use this useful operating system environment variable: 30 | 31 | ```bash 32 | $ export JAVA_OPTS=-Xmx1024m -XX:MaxPermSize=128M -Djava.security.egd=file:/dev/./urandom 33 | ``` 34 | 35 | or... 36 | 37 | You can build a single executable JAR file that contains all the necessary dependencies, classes, and resources. 38 | 39 | ```bash 40 | ./gradlew clean build 41 | ``` 42 | 43 | Then you can run the JAR file: 44 | 45 | ```bash 46 | java -jar build/libs/LoftPage-0.0.2.jar 47 | ``` 48 | 49 | ## Animation (gif file,please wait!): 50 | 51 | ![](https://github.com/decaywood/LoftPage/blob/master/Info/2048.gif) 52 | 53 | default port is 4000, when App is running, input {your server IP}:4000 in your browser. 54 | 55 | enjoy! 56 | -------------------------------------------------------------------------------- /src/main/resources/static/js/local_storage_manager.js: -------------------------------------------------------------------------------- 1 | window.fakeStorage = { 2 | _data: {}, 3 | 4 | setItem: function (id, val) { 5 | return this._data[id] = String(val); 6 | }, 7 | 8 | getItem: function (id) { 9 | return this._data.hasOwnProperty(id) ? this._data[id] : undefined; 10 | }, 11 | 12 | removeItem: function (id) { 13 | return delete this._data[id]; 14 | }, 15 | 16 | clear: function () { 17 | return this._data = {}; 18 | } 19 | }; 20 | 21 | function LocalStorageManager() { 22 | this.bestScoreKey = "bestScore"; 23 | this.gameStateKey = "gameState"; 24 | 25 | var supported = this.localStorageSupported(); 26 | this.storage = supported ? window.localStorage : window.fakeStorage; 27 | } 28 | 29 | LocalStorageManager.prototype.localStorageSupported = function () { 30 | var testKey = "test"; 31 | var storage = window.localStorage; 32 | 33 | try { 34 | storage.setItem(testKey, "1"); 35 | storage.removeItem(testKey); 36 | return true; 37 | } catch (error) { 38 | return false; 39 | } 40 | }; 41 | 42 | // Best score getters/setters 43 | LocalStorageManager.prototype.getBestScore = function () { 44 | return this.storage.getItem(this.bestScoreKey) || 0; 45 | }; 46 | 47 | LocalStorageManager.prototype.setBestScore = function (score) { 48 | this.storage.setItem(this.bestScoreKey, score); 49 | }; 50 | 51 | // Game state getters/setters and clearing 52 | LocalStorageManager.prototype.getGameState = function () { 53 | var stateJSON = this.storage.getItem(this.gameStateKey); 54 | return stateJSON ? JSON.parse(stateJSON) : null; 55 | }; 56 | 57 | LocalStorageManager.prototype.setGameState = function (gameState) { 58 | this.storage.setItem(this.gameStateKey, JSON.stringify(gameState)); 59 | }; 60 | 61 | LocalStorageManager.prototype.clearGameState = function () { 62 | this.storage.removeItem(this.gameStateKey); 63 | }; 64 | -------------------------------------------------------------------------------- /src/main/resources/static/css/helpers.scss: -------------------------------------------------------------------------------- 1 | // Exponent 2 | // From: https://github.com/Team-Sass/Sassy-math/blob/master/sass/math.scss#L36 3 | 4 | @function exponent($base, $exponent) { 5 | // reset value 6 | $value: $base; 7 | // positive intergers get multiplied 8 | @if $exponent > 1 { 9 | @for $i from 2 through $exponent { 10 | $value: $value * $base; } } 11 | // negitive intergers get divided. A number divided by itself is 1 12 | @if $exponent < 1 { 13 | @for $i from 0 through -$exponent { 14 | $value: $value / $base; } } 15 | // return the last value written 16 | @return $value; 17 | } 18 | 19 | @function pow($base, $exponent) { 20 | @return exponent($base, $exponent); 21 | } 22 | 23 | // Transition mixins 24 | @mixin transition($args...) { 25 | -webkit-transition: $args; 26 | -moz-transition: $args; 27 | -ms-transform: $args; 28 | transition: $args; 29 | } 30 | 31 | @mixin transition-property($args...) { 32 | -webkit-transition-property: $args; 33 | -moz-transition-property: $args; 34 | transition-property: $args; 35 | } 36 | 37 | @mixin animation($args...) { 38 | -webkit-animation: $args; 39 | -moz-animation: $args; 40 | animation: $args; 41 | } 42 | 43 | @mixin animation-fill-mode($args...) { 44 | -webkit-animation-fill-mode: $args; 45 | -moz-animation-fill-mode: $args; 46 | animation-fill-mode: $args; 47 | } 48 | 49 | @mixin transform($args...) { 50 | -webkit-transform: $args; 51 | -moz-transform: $args; 52 | transform: $args; 53 | } 54 | 55 | // Keyframe animations 56 | //@mixin keyframes($animation-name) { 57 | // @-webkit-keyframes $animation-name { 58 | // @content; 59 | // } 60 | // @-moz-keyframes $animation-name { 61 | // @content; 62 | // } 63 | // @keyframes $animation-name { 64 | // @content; 65 | // } 66 | //} 67 | 68 | // Media queries 69 | @mixin smaller($width) { 70 | @media screen and (max-width: $width) { 71 | @content; 72 | } 73 | } 74 | 75 | // Clearfix 76 | @mixin clearfix { 77 | &:after { 78 | content: ""; 79 | display: block; 80 | clear: both; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/resources/static/js/classlist_polyfill.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | if (typeof window.Element === "undefined" || 3 | "classList" in document.documentElement) { 4 | return; 5 | } 6 | 7 | var prototype = Array.prototype, 8 | push = prototype.push, 9 | splice = prototype.splice, 10 | join = prototype.join; 11 | 12 | function DOMTokenList(el) { 13 | this.el = el; 14 | // The className needs to be trimmed and split on whitespace 15 | // to retrieve a list of classes. 16 | var classes = el.className.replace(/^\s+|\s+$/g, '').split(/\s+/); 17 | for (var i = 0; i < classes.length; i++) { 18 | push.call(this, classes[i]); 19 | } 20 | } 21 | 22 | DOMTokenList.prototype = { 23 | add: function (token) { 24 | if (this.contains(token)) return; 25 | push.call(this, token); 26 | this.el.className = this.toString(); 27 | }, 28 | contains: function (token) { 29 | return this.el.className.indexOf(token) != -1; 30 | }, 31 | item: function (index) { 32 | return this[index] || null; 33 | }, 34 | remove: function (token) { 35 | if (!this.contains(token)) return; 36 | for (var i = 0; i < this.length; i++) { 37 | if (this[i] == token) break; 38 | } 39 | splice.call(this, i, 1); 40 | this.el.className = this.toString(); 41 | }, 42 | toString: function () { 43 | return join.call(this, ' '); 44 | }, 45 | toggle: function (token) { 46 | if (!this.contains(token)) { 47 | this.add(token); 48 | } else { 49 | this.remove(token); 50 | } 51 | 52 | return this.contains(token); 53 | } 54 | }; 55 | 56 | window.DOMTokenList = DOMTokenList; 57 | 58 | function defineElementGetter(obj, prop, getter) { 59 | if (Object.defineProperty) { 60 | Object.defineProperty(obj, prop, { 61 | get: getter 62 | }); 63 | } else { 64 | obj.__defineGetter__(prop, getter); 65 | } 66 | } 67 | 68 | defineElementGetter(HTMLElement.prototype, 'classList', function () { 69 | return new DOMTokenList(this); 70 | }); 71 | })(); 72 | -------------------------------------------------------------------------------- /src/main/java/org/decaywood/buffer/MultiSendersBuffer.java: -------------------------------------------------------------------------------- 1 | package org.decaywood.buffer; 2 | 3 | import com.lmax.disruptor.EventHandler; 4 | import com.lmax.disruptor.SleepingWaitStrategy; 5 | import com.lmax.disruptor.dsl.Disruptor; 6 | import com.lmax.disruptor.dsl.ProducerType; 7 | import org.decaywood.KeyEvent; 8 | import org.decaywood.buffer.handler.KeyEventSender; 9 | import org.springframework.stereotype.Component; 10 | 11 | import javax.annotation.Resource; 12 | import java.util.concurrent.Executors; 13 | import java.util.function.Supplier; 14 | 15 | /** 16 | * @author: decaywood 17 | * @date: 2015/6/20 18 | * 19 | */ 20 | 21 | @Component(value = "MultiSendersBuffer") 22 | public class MultiSendersBuffer extends MainBuffer { 23 | 24 | private int bufferSize; 25 | private Supplier[]> generator; 26 | 27 | @Resource 28 | public void setKeyEventSender(KeyEventSender sender) { 29 | this.keyEventSender = sender; 30 | initBuffer(bufferSize, generator); 31 | } 32 | private KeyEventSender keyEventSender; 33 | 34 | public MultiSendersBuffer() { 35 | this(() -> new EventHandler[]{}); 36 | } 37 | 38 | /** 39 | * generate the handlers like logger handler, sql handler and so on 40 | */ 41 | 42 | public MultiSendersBuffer(Supplier[]> generator) { 43 | this(1 << 10, generator); 44 | } 45 | 46 | /** 47 | * @param bufferSize ringBuffer Size 48 | */ 49 | public MultiSendersBuffer(int bufferSize, Supplier[]> generator) { 50 | this.bufferSize = bufferSize; 51 | this.generator = generator; 52 | } 53 | 54 | private void initBuffer(int bufferSize, Supplier[]> generator) { 55 | 56 | int size = bufferSize > 0 ? bufferSize : 1 << 10; 57 | 58 | Disruptor disruptor = new Disruptor<>( 59 | KeyEvent::new, 60 | size, 61 | Executors.defaultThreadFactory(), 62 | ProducerType.SINGLE, 63 | new SleepingWaitStrategy()); 64 | 65 | EventHandler[] src = generator.get(); 66 | EventHandler[] handlers = new EventHandler[src.length + 1]; 67 | handlers[0] = keyEventSender; 68 | System.out.println("key event sender : " + keyEventSender); 69 | System.arraycopy(src, 0, handlers, 1, src.length); 70 | System.out.println("handler len : " + handlers.length); 71 | disruptor.handleEventsWith(handlers); 72 | 73 | this.ringBuffer = disruptor.start(); 74 | 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /src/main/resources/static/js/grid.js: -------------------------------------------------------------------------------- 1 | function Grid(size, previousState) { 2 | this.size = size; 3 | this.cells = previousState ? this.fromState(previousState) : this.empty(); 4 | } 5 | 6 | // Build a grid of the specified size 7 | Grid.prototype.empty = function () { 8 | var cells = []; 9 | 10 | for (var x = 0; x < this.size; x++) { 11 | var row = cells[x] = []; 12 | 13 | for (var y = 0; y < this.size; y++) { 14 | row.push(null); 15 | } 16 | } 17 | 18 | return cells; 19 | }; 20 | 21 | Grid.prototype.fromState = function (state) { 22 | var cells = []; 23 | 24 | for (var x = 0; x < this.size; x++) { 25 | var row = cells[x] = []; 26 | 27 | for (var y = 0; y < this.size; y++) { 28 | var tile = state[x][y]; 29 | row.push(tile ? new Tile(tile.position, tile.value) : null); 30 | } 31 | } 32 | 33 | return cells; 34 | }; 35 | 36 | // Find the first available random position 37 | Grid.prototype.randomAvailableCell = function () { 38 | var cells = this.availableCells(); 39 | 40 | if (cells.length) { 41 | return cells[Math.floor(Math.random() * cells.length)]; 42 | } 43 | }; 44 | 45 | Grid.prototype.availableCells = function () { 46 | var cells = []; 47 | 48 | this.eachCell(function (x, y, tile) { 49 | if (!tile) { 50 | cells.push({ x: x, y: y }); 51 | } 52 | }); 53 | 54 | return cells; 55 | }; 56 | 57 | // Call callback for every cell 58 | Grid.prototype.eachCell = function (callback) { 59 | for (var x = 0; x < this.size; x++) { 60 | for (var y = 0; y < this.size; y++) { 61 | callback(x, y, this.cells[x][y]); 62 | } 63 | } 64 | }; 65 | 66 | // Check if there are any cells available 67 | Grid.prototype.cellsAvailable = function () { 68 | return !!this.availableCells().length; 69 | }; 70 | 71 | // Check if the specified cell is taken 72 | Grid.prototype.cellAvailable = function (cell) { 73 | return !this.cellOccupied(cell); 74 | }; 75 | 76 | Grid.prototype.cellOccupied = function (cell) { 77 | return !!this.cellContent(cell); 78 | }; 79 | 80 | Grid.prototype.cellContent = function (cell) { 81 | if (this.withinBounds(cell)) { 82 | return this.cells[cell.x][cell.y]; 83 | } else { 84 | return null; 85 | } 86 | }; 87 | 88 | // Inserts a tile at its position 89 | Grid.prototype.insertTile = function (tile) { 90 | this.cells[tile.x][tile.y] = tile; 91 | }; 92 | 93 | Grid.prototype.removeTile = function (tile) { 94 | this.cells[tile.x][tile.y] = null; 95 | }; 96 | 97 | Grid.prototype.withinBounds = function (position) { 98 | return position.x >= 0 && position.x < this.size && 99 | position.y >= 0 && position.y < this.size; 100 | }; 101 | 102 | Grid.prototype.serialize = function () { 103 | var cellState = []; 104 | 105 | for (var x = 0; x < this.size; x++) { 106 | var row = cellState[x] = []; 107 | 108 | for (var y = 0; y < this.size; y++) { 109 | row.push(this.cells[x][y] ? this.cells[x][y].serialize() : null); 110 | } 111 | } 112 | 113 | return { 114 | size: this.size, 115 | cells: cellState 116 | }; 117 | }; 118 | -------------------------------------------------------------------------------- /src/main/java/org/decaywood/buffer/handler/KeyEventSender.java: -------------------------------------------------------------------------------- 1 | package org.decaywood.buffer.handler; 2 | 3 | import com.lmax.disruptor.EventHandler; 4 | import org.decaywood.KeyEvent; 5 | import org.decaywood.KeyEventSequencer; 6 | import org.decaywood.serve.ConnectionManager; 7 | import org.springframework.messaging.simp.SimpMessagingTemplate; 8 | import org.springframework.stereotype.Component; 9 | 10 | import javax.annotation.Resource; 11 | import java.util.function.Consumer; 12 | 13 | /** 14 | * @author: decaywood 15 | * @date: 2015/6/19 9:56 16 | */ 17 | 18 | @Component(value = "KeyEventSender") 19 | public class KeyEventSender implements EventHandler { 20 | 21 | public static final String ADDRESS_PREFIX = "/message/responds/"; 22 | 23 | /** 24 | * 25 | * The messages were sending in ascending order from the application implementation perspective 26 | * (I.e, convertAndSend() are called in one thread or at least thread safe fashion"). 27 | * However, Springframework web socket uses reactor-tcp implementation which will process the messages 28 | * on clientOutboundChannel from the thread pool. Thus the messages can be written to the tcp socket 29 | * in different order that they are arrived. 30 | * 31 | * solution: 32 | * 1. configured the web socket to limit one thread for the clientOutboundChannel. 33 | * 2. write our own logical to preserve the order --> we accept this solution,the main reason as follow: 34 | * 35 | * * back end required high react time,so processing messages by threadPool is needed. 36 | * * we don't care the cost of frontend(relatively). 37 | * 38 | */ 39 | @Resource 40 | private SimpMessagingTemplate simpMessagingTemplate; 41 | 42 | /** 43 | * it is used to manage the connection, mapping URL between two gamer 44 | */ 45 | @Resource(name = "ConnectionManager") 46 | protected ConnectionManager manager; 47 | 48 | /** 49 | * mainBuffer would disorder the keyEvent,sequencer can 50 | * reorder the keyEvent passed through the mainBuffer 51 | */ 52 | @Resource(name = "KeyEventSequencer") 53 | protected KeyEventSequencer sequencer; 54 | 55 | public KeyEventSender() {} 56 | 57 | private void execute(KeyEvent event) throws InterruptedException { 58 | Consumer optional = this::sendEvent; 59 | sequencer.processKeyEvent(event, optional); 60 | } 61 | 62 | public void sendEvent(KeyEvent event) { 63 | 64 | try { 65 | 66 | String sendURL; 67 | String IPAddress = event.getIPAddress(); 68 | String userID = event.getUserID(); 69 | sendURL = manager.getSendURL(IPAddress, userID); 70 | simpMessagingTemplate.convertAndSend(sendURL, event); 71 | System.out.println("send Event : " + event.getGameState() + " " + sendURL); 72 | 73 | } catch (Exception e) { 74 | simpMessagingTemplate.convertAndSend(KeyEventSender.ADDRESS_PREFIX 75 | + event.getUserID(), e.getMessage()); 76 | } 77 | } 78 | 79 | @Override 80 | public void onEvent(KeyEvent event, long sequence, boolean endOfBatch) throws Exception { 81 | execute(event); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/resources/static/js/uuid.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Math.uuid.js (v1.4) 3 | http://www.broofa.com 4 | mailto:robert@broofa.com 5 | 6 | Copyright (c) 2010 Robert Kieffer 7 | Dual licensed under the MIT and GPL licenses. 8 | */ 9 | 10 | /* 11 | * Generate a random uuid. 12 | * 13 | * USAGE: Math.uuid(length, radix) 14 | * length - the desired number of characters 15 | * radix - the number of allowable values for each character. 16 | * 17 | * EXAMPLES: 18 | * // No arguments - returns RFC4122, version 4 ID 19 | * >>> Math.uuid() 20 | * "92329D39-6F5C-4520-ABFC-AAB64544E172" 21 | * 22 | * // One argument - returns ID of the specified length 23 | * >>> Math.uuid(15) // 15 character ID (default base=62) 24 | * "VcydxgltxrVZSTV" 25 | * 26 | * // Two arguments - returns ID of the specified length, and radix. (Radix must be <= 62) 27 | * >>> Math.uuid(8, 2) // 8 character ID (base=2) 28 | * "01001010" 29 | * >>> Math.uuid(8, 10) // 8 character ID (base=10) 30 | * "47473046" 31 | * >>> Math.uuid(8, 16) // 8 character ID (base=16) 32 | * "098F4D35" 33 | */ 34 | (function() { 35 | // Private array of chars to use 36 | var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); 37 | 38 | Math.uuid = function (len, radix) { 39 | var chars = CHARS, uuid = [], i; 40 | radix = radix || chars.length; 41 | 42 | if (len) { 43 | // Compact form 44 | for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix]; 45 | } else { 46 | // rfc4122, version 4 form 47 | var r; 48 | 49 | // rfc4122 requires these characters 50 | uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'; 51 | uuid[14] = '4'; 52 | 53 | // Fill in random data. At i==19 set the high bits of clock sequence as 54 | // per rfc4122, sec. 4.1.5 55 | for (i = 0; i < 36; i++) { 56 | if (!uuid[i]) { 57 | r = 0 | Math.random()*16; 58 | uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; 59 | } 60 | } 61 | } 62 | 63 | return uuid.join(''); 64 | }; 65 | 66 | // A more performant, but slightly bulkier, RFC4122v4 solution. We boost performance 67 | // by minimizing calls to random() 68 | Math.uuidFast = function() { 69 | var chars = CHARS, uuid = new Array(36), rnd=0, r; 70 | for (var i = 0; i < 36; i++) { 71 | if (i==8 || i==13 || i==18 || i==23) { 72 | uuid[i] = '-'; 73 | } else if (i==14) { 74 | uuid[i] = '4'; 75 | } else { 76 | if (rnd <= 0x02) rnd = 0x2000000 + (Math.random()*0x1000000)|0; 77 | r = rnd & 0xf; 78 | rnd = rnd >> 4; 79 | uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; 80 | } 81 | } 82 | return uuid.join(''); 83 | }; 84 | 85 | // A more compact, but less performant, RFC4122v4 solution: 86 | Math.uuidCompact = function() { 87 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 88 | var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); 89 | return v.toString(16); 90 | }); 91 | }; 92 | })(); -------------------------------------------------------------------------------- /src/main/resources/static/js/html_actuator.js: -------------------------------------------------------------------------------- 1 | function HTMLActuator(target) { 2 | this.tileContainer = document.querySelector(target+"-tile-container"); 3 | this.scoreContainer = document.querySelector(target+"-score-container"); 4 | this.bestContainer = document.querySelector(target+"-best-container"); 5 | this.messageContainer = document.querySelector(target+"-game-message"); 6 | this.ipContainer = document.querySelector(target+"-ip-container"); 7 | this.score = 0; 8 | 9 | } 10 | 11 | HTMLActuator.prototype.actuate = function (grid, metadata) { 12 | var self = this; 13 | 14 | window.requestAnimationFrame(function () { 15 | self.clearContainer(self.tileContainer); 16 | 17 | grid.cells.forEach(function (column) { 18 | column.forEach(function (cell) { 19 | if (cell) { 20 | self.addTile(cell); 21 | } 22 | }); 23 | }); 24 | 25 | self.updateScore(metadata.score); 26 | self.updateBestScore(metadata.bestScore); 27 | 28 | if (metadata.terminated) { 29 | if (metadata.over) { 30 | self.message(false); // You lose 31 | } else if (metadata.won) { 32 | self.message(true); // You win! 33 | } 34 | } 35 | 36 | }); 37 | }; 38 | 39 | // Continues the game (both restart and keep playing) 40 | HTMLActuator.prototype.continueGame = function () { 41 | this.clearMessage(); 42 | }; 43 | 44 | HTMLActuator.prototype.clearContainer = function (container) { 45 | while (container.firstChild) { 46 | container.removeChild(container.firstChild); 47 | } 48 | }; 49 | 50 | HTMLActuator.prototype.addTile = function (tile) { 51 | var self = this; 52 | 53 | var wrapper = document.createElement("div"); 54 | var inner = document.createElement("div"); 55 | var position = tile.previousPosition || { x: tile.x, y: tile.y }; 56 | var positionClass = this.positionClass(position); 57 | 58 | // We can't use classlist because it somehow glitches when replacing classes 59 | var classes = ["tile", "tile-" + tile.value, positionClass]; 60 | 61 | if (tile.value > 2048) classes.push("tile-super"); 62 | 63 | this.applyClasses(wrapper, classes); 64 | 65 | inner.classList.add("tile-inner"); 66 | inner.textContent = tile.value; 67 | 68 | if (tile.previousPosition) { 69 | // Make sure that the tile gets rendered in the previous position first 70 | window.requestAnimationFrame(function () { 71 | classes[2] = self.positionClass({ x: tile.x, y: tile.y }); 72 | self.applyClasses(wrapper, classes); // Update the position 73 | }); 74 | } else if (tile.mergedFrom) { 75 | classes.push("tile-merged"); 76 | this.applyClasses(wrapper, classes); 77 | 78 | // Render the tiles that merged 79 | tile.mergedFrom.forEach(function (merged) { 80 | self.addTile(merged); 81 | }); 82 | } else { 83 | classes.push("tile-new"); 84 | this.applyClasses(wrapper, classes); 85 | } 86 | 87 | // Add the inner part of the tile to the wrapper 88 | wrapper.appendChild(inner); 89 | 90 | // Put the tile on the board 91 | this.tileContainer.appendChild(wrapper); 92 | }; 93 | 94 | HTMLActuator.prototype.applyClasses = function (element, classes) { 95 | element.setAttribute("class", classes.join(" ")); 96 | }; 97 | 98 | HTMLActuator.prototype.normalizePosition = function (position) { 99 | return { x: position.x + 1, y: position.y + 1 }; 100 | }; 101 | 102 | HTMLActuator.prototype.positionClass = function (position) { 103 | position = this.normalizePosition(position); 104 | return "tile-position-" + position.x + "-" + position.y; 105 | }; 106 | 107 | HTMLActuator.prototype.updateIPAddress = function (IPAddress) { 108 | if(IPAddress == null || IPAddress == undefined) return; 109 | this.ipContainer.textContent ="Guest IP :" + IPAddress; 110 | }; 111 | 112 | HTMLActuator.prototype.updateScore = function (score) { 113 | this.clearContainer(this.scoreContainer); 114 | 115 | var difference = score - this.score; 116 | this.score = score; 117 | 118 | this.scoreContainer.textContent = this.score; 119 | 120 | if (difference > 0) { 121 | var addition = document.createElement("div"); 122 | addition.classList.add("score-addition"); 123 | addition.textContent = "+" + difference; 124 | 125 | this.scoreContainer.appendChild(addition); 126 | } 127 | }; 128 | 129 | HTMLActuator.prototype.updateBestScore = function (bestScore) { 130 | this.bestContainer.textContent = bestScore; 131 | }; 132 | 133 | HTMLActuator.prototype.message = function (won) { 134 | var type = won ? "game-won" : "game-over"; 135 | var message = won ? "You win!" : "Game over!"; 136 | 137 | this.messageContainer.classList.add(type); 138 | this.messageContainer.getElementsByTagName("p")[0].textContent = message; 139 | }; 140 | 141 | HTMLActuator.prototype.clearMessage = function () { 142 | // IE only takes one value to remove at a time. 143 | this.messageContainer.classList.remove("game-won"); 144 | this.messageContainer.classList.remove("game-over"); 145 | }; 146 | -------------------------------------------------------------------------------- /src/main/resources/static/js/keyboard_input_manager.js: -------------------------------------------------------------------------------- 1 | function KeyboardInputManager(netSendManager) { 2 | 3 | // Respond to button presses 4 | this.bindButtonPress(".retry-button", this.connect); 5 | this.bindButtonPress(".connect-button", this.connect); 6 | this.bindButtonPress(".keep-playing-button", this.keepPlaying); 7 | 8 | this.netSendManager = netSendManager; 9 | this.events = {}; 10 | 11 | if (window.navigator.msPointerEnabled) { 12 | //Internet Explorer 10 style 13 | this.eventTouchstart = "MSPointerDown"; 14 | this.eventTouchmove = "MSPointerMove"; 15 | this.eventTouchend = "MSPointerUp"; 16 | } else { 17 | this.eventTouchstart = "touchstart"; 18 | this.eventTouchmove = "touchmove"; 19 | this.eventTouchend = "touchend"; 20 | } 21 | 22 | this.listen(); 23 | } 24 | 25 | KeyboardInputManager.prototype.on = function (event, callback) { 26 | if (!this.events[event]) { 27 | this.events[event] = []; 28 | } 29 | this.events[event].push(callback); 30 | }; 31 | 32 | KeyboardInputManager.prototype.emit = function (event, data) { 33 | var callbacks = this.events[event]; 34 | if (callbacks) { 35 | callbacks.forEach(function (callback) { 36 | callback(data); 37 | }); 38 | } 39 | }; 40 | 41 | KeyboardInputManager.prototype.listen = function () { 42 | var self = this; 43 | var netSendManager = this.netSendManager; 44 | 45 | var map = { 46 | 38: 0, // Up 47 | 39: 1, // Right 48 | 40: 2, // Down 49 | 37: 3, // Left 50 | 75: 0, // Vim up 51 | 76: 1, // Vim right 52 | 74: 2, // Vim down 53 | 72: 3, // Vim left 54 | 87: 0, // W 55 | 68: 1, // D 56 | 83: 2, // S 57 | 65: 3 // A 58 | }; 59 | 60 | // Respond to direction keys 61 | document.addEventListener("keydown", function (event) { 62 | var modifiers = event.altKey || event.ctrlKey || event.metaKey || 63 | event.shiftKey; 64 | var mapped = map[event.which]; 65 | 66 | if(!modifiers) { 67 | if (mapped !== undefined) { 68 | event.preventDefault(); 69 | self.emit("move", mapped); 70 | } 71 | 72 | 73 | // R key connect the game 74 | if (event.which === 82) { 75 | self.connect.call(self, event); 76 | } 77 | 78 | netSendManager.sendGameState(event); 79 | 80 | } 81 | 82 | }); 83 | 84 | // Respond to swipe events 85 | var touchStartClientX, touchStartClientY; 86 | var gameContainer = document.getElementsByClassName("game-container")[0]; 87 | 88 | gameContainer.addEventListener(this.eventTouchstart, function (event) { 89 | 90 | if ((!window.navigator.msPointerEnabled && event.touches.length > 1) || 91 | event.targetTouches.length > 1) { 92 | return; // Ignore if touching with more than 1 finger 93 | } 94 | 95 | if (window.navigator.msPointerEnabled) { 96 | touchStartClientX = event.pageX; 97 | touchStartClientY = event.pageY; 98 | } else { 99 | touchStartClientX = event.touches[0].clientX; 100 | touchStartClientY = event.touches[0].clientY; 101 | } 102 | 103 | event.preventDefault(); 104 | }); 105 | 106 | gameContainer.addEventListener(this.eventTouchmove, function (event) { 107 | event.preventDefault(); 108 | }); 109 | 110 | gameContainer.addEventListener(this.eventTouchend, function (event) { 111 | if ((!window.navigator.msPointerEnabled && event.touches.length > 0) || 112 | event.targetTouches.length > 0) { 113 | return; // Ignore if still touching with one or more fingers 114 | } 115 | 116 | var touchEndClientX, touchEndClientY; 117 | 118 | if (window.navigator.msPointerEnabled) { 119 | touchEndClientX = event.pageX; 120 | touchEndClientY = event.pageY; 121 | } else { 122 | touchEndClientX = event.changedTouches[0].clientX; 123 | touchEndClientY = event.changedTouches[0].clientY; 124 | } 125 | 126 | var dx = touchEndClientX - touchStartClientX; 127 | var absDx = Math.abs(dx); 128 | 129 | var dy = touchEndClientY - touchStartClientY; 130 | var absDy = Math.abs(dy); 131 | 132 | if (Math.max(absDx, absDy) > 10) { 133 | // (right : left) : (down : up) 134 | var which = absDx > absDy ? (dx > 0 ? 1 : 3) : (dy > 0 ? 2 : 0); 135 | self.emit("move", which); 136 | netSendManager.sendGameState({which:absDx > absDy ? (dx > 0 ? 39 : 37) : (dy > 0 ? 40 : 38)}); 137 | } 138 | }); 139 | }; 140 | 141 | KeyboardInputManager.prototype.connect = function (event) { 142 | event.preventDefault(); 143 | this.emit("connect"); 144 | }; 145 | 146 | KeyboardInputManager.prototype.keepPlaying = function (event) { 147 | event.preventDefault(); 148 | this.emit("keepPlaying"); 149 | }; 150 | 151 | KeyboardInputManager.prototype.bindButtonPress = function (selector, fn) { 152 | var button = document.querySelector(selector); 153 | button.addEventListener("click", fn.bind(this)); 154 | button.addEventListener(this.eventTouchend, fn.bind(this)); 155 | }; 156 | -------------------------------------------------------------------------------- /src/main/java/org/decaywood/buffer/MessageBuffer.java: -------------------------------------------------------------------------------- 1 | package org.decaywood.buffer; 2 | 3 | import org.decaywood.KeyEvent; 4 | import org.springframework.stereotype.Component; 5 | 6 | import javax.annotation.Resource; 7 | import java.util.Queue; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | import java.util.concurrent.ConcurrentLinkedDeque; 10 | import java.util.concurrent.ExecutorService; 11 | import java.util.concurrent.Executors; 12 | import java.util.concurrent.atomic.LongAdder; 13 | 14 | /** 15 | * @author: decaywood 16 | * @date: 2015/6/18 20:42 17 | */ 18 | 19 | @Component(value = "MessageBuffer") 20 | public class MessageBuffer implements Runnable { 21 | 22 | @Resource(name = "MultiSendersBuffer") 23 | private MainBuffer mainBuffer; 24 | 25 | /** 26 | * Queue is maintained by bufferPool, Queues can reduce the thread write conflict 27 | * 28 | * 29 | * client -> Queue 1 client 30 | * \__ __/ 31 | * \__ __________ __/ 32 | * \ | | / 33 | * client -> Queue 2-------+==========*MainBuffer*==> Sender =====+------ client 34 | * * __/ |__________| \__ 35 | * * __/ \__ 36 | * * / \ 37 | * client -> Queue N client 38 | */ 39 | 40 | 41 | private ConcurrentHashMap> bufferPool; 42 | private ExecutorService service; 43 | private LongAdder messageCounter; 44 | 45 | /** 46 | * if the average buffer size overweight the threshold 47 | * it would trigger the dispatch thread to publish keyEvent 48 | */ 49 | private int bufferIncreaseThreshold; 50 | private int flushThreshold; 51 | 52 | 53 | 54 | public MessageBuffer() { 55 | this(1 << 10, 1, Runtime.getRuntime().availableProcessors()); 56 | } 57 | 58 | 59 | /** 60 | * 61 | * @param bufferIncreaseThreshold when average size per buffer outweight the shredshold, 62 | * new buffer will be added into the pool. 63 | * @param flushThreshold when total message in the buffer pool outweight the shredshold, 64 | * every buffer will be flushed into main buffer. 65 | * @param cacheSize the origin buffer count. 66 | * 67 | */ 68 | public MessageBuffer(int bufferIncreaseThreshold, int flushThreshold, int cacheSize) { 69 | 70 | init(bufferIncreaseThreshold, flushThreshold, cacheSize); 71 | 72 | } 73 | 74 | private void init(int bufferIncreaseThreshold, int flushThreshold, int cacheSize) { 75 | 76 | this.service = Executors.newSingleThreadExecutor(); 77 | 78 | this.bufferIncreaseThreshold = bufferIncreaseThreshold > 0 ? bufferIncreaseThreshold : 1 << 8; 79 | this.flushThreshold = flushThreshold; 80 | 81 | this.messageCounter = new LongAdder(); 82 | this.bufferPool = new ConcurrentHashMap<>(); 83 | for (int i = 0; i < cacheSize; i++) { 84 | this.bufferPool.put(i, new ConcurrentLinkedDeque<>()); 85 | } 86 | 87 | } 88 | 89 | 90 | 91 | public void publishEvent(KeyEvent keyEvent) { 92 | 93 | messageCounter.increment(); 94 | Queue buffer = getMinBuffer(); 95 | buffer.offer(keyEvent); 96 | flushBufferToMainBuffer(); 97 | 98 | } 99 | 100 | private void flushBufferToMainBuffer() { 101 | 102 | if(messageCounter.intValue() < flushThreshold) return; 103 | this.service.execute(this); 104 | 105 | } 106 | 107 | private Queue getMinBuffer() { 108 | if(averageSize() > this.bufferIncreaseThreshold){ 109 | this.bufferPool.put(this.bufferPool.size(), new ConcurrentLinkedDeque<>()); 110 | } 111 | return bufferPool.reduceValues( 112 | Integer.MAX_VALUE, 113 | (first, second) -> first.size() < second.size() ? first : second); 114 | } 115 | 116 | 117 | private int averageSize() { 118 | int sizeSum = bufferPool.reduceToInt( 119 | Integer.MAX_VALUE, 120 | (key, value) -> value.size(), 121 | 0, 122 | (first, second) -> first + second); 123 | return sizeSum / bufferPool.size(); 124 | } 125 | 126 | 127 | public int getPoolSize() { 128 | return this.bufferPool.size(); 129 | } 130 | 131 | @Override 132 | public void run() { 133 | 134 | this.bufferPool.forEachValue(Integer.MAX_VALUE, 135 | (value) -> { 136 | while (value.size() > 0) { 137 | messageCounter.decrement(); 138 | KeyEvent event = value.poll(); 139 | this.mainBuffer.publishKeyEvent(event); 140 | } 141 | }); 142 | 143 | } 144 | 145 | 146 | } 147 | -------------------------------------------------------------------------------- /src/main/java/org/decaywood/serve/ConnectionManager.java: -------------------------------------------------------------------------------- 1 | package org.decaywood.serve; 2 | 3 | import org.decaywood.KeyEvent; 4 | import org.decaywood.buffer.handler.KeyEventSender; 5 | import org.springframework.messaging.simp.SimpMessagingTemplate; 6 | import org.springframework.stereotype.Component; 7 | 8 | import javax.annotation.Resource; 9 | import java.util.Deque; 10 | import java.util.LinkedList; 11 | import java.util.Map; 12 | import java.util.concurrent.ConcurrentHashMap; 13 | 14 | /** 15 | * @author: decaywood 16 | * @date: 2015/6/29 21:48 17 | */ 18 | 19 | @Component(value = "ConnectionManager") 20 | public class ConnectionManager { 21 | 22 | @Resource 23 | private SimpMessagingTemplate template; 24 | 25 | /** 26 | * connectionQueue is defined as a waiting queue 27 | * when two user is waiting for connection,these two 28 | * user can be matched into IDMapper 29 | */ 30 | private Deque connectionQueue; 31 | 32 | /** 33 | * IDMapper record the user mapping information, 34 | * when user1 send the message to user2, eventSender 35 | * can send the message to two users according to 36 | * the IDMapper 37 | */ 38 | private Map IDMapper; 39 | 40 | public static class URLMapper { 41 | 42 | private final String IPAddress; 43 | private final String userID; 44 | 45 | public URLMapper(String IPAddress, String userID) { 46 | this.IPAddress = IPAddress; 47 | this.userID = userID; 48 | } 49 | 50 | @Override 51 | public boolean equals(Object o) { 52 | if (this == o) return true; 53 | if (o == null || getClass() != o.getClass()) return false; 54 | 55 | URLMapper urlMapper = (URLMapper) o; 56 | 57 | return IPAddress.equals(urlMapper.IPAddress); 58 | 59 | } 60 | 61 | @Override 62 | public int hashCode() { 63 | return IPAddress.hashCode(); 64 | } 65 | } 66 | 67 | public ConnectionManager() { 68 | this.connectionQueue = new LinkedList<>(); 69 | this.IDMapper = new ConcurrentHashMap<>(); 70 | } 71 | 72 | public void disConnectGame(URLMapper mapper, KeyEvent event) { 73 | disConnectGame(event, mapper.IPAddress, mapper.userID); 74 | } 75 | 76 | public synchronized void disConnectGame(KeyEvent event, String IPAddress, String userID) { 77 | 78 | URLMapper urlMapper = new URLMapper(IPAddress, userID); 79 | 80 | if(!IDMapper.containsKey(urlMapper)) return; 81 | 82 | URLMapper remote = IDMapper.get(urlMapper); 83 | IDMapper.remove(urlMapper); 84 | 85 | if(IDMapper.containsKey(remote)) IDMapper.remove(remote); 86 | 87 | event.setGameState("game_disconnect"); 88 | template.convertAndSend(KeyEventSender.ADDRESS_PREFIX + userID, event); 89 | 90 | changEventContent(remote, event); 91 | template.convertAndSend(KeyEventSender.ADDRESS_PREFIX + remote.userID, event); 92 | 93 | changEventContent(urlMapper, event); 94 | 95 | } 96 | 97 | 98 | public synchronized String connect(KeyEvent keyEvent) { 99 | 100 | String IPAddress = keyEvent.getIPAddress(); 101 | String userID = keyEvent.getUserID(); 102 | 103 | URLMapper mapper = new URLMapper(IPAddress, userID); 104 | 105 | while (true) { 106 | if (!connectionQueue.isEmpty() && !IDMapper.containsKey(mapper)) { 107 | 108 | if (mapper.equals(connectionQueue.peek())) return "Waiting For Remote Connection!"; 109 | 110 | URLMapper remote = connectionQueue.poll(); 111 | 112 | IDMapper.put(mapper, remote); 113 | IDMapper.put(remote, mapper); 114 | 115 | keyEvent.setGameState("game_connect_ack"); 116 | 117 | template.convertAndSend(KeyEventSender.ADDRESS_PREFIX + userID, keyEvent); 118 | 119 | changEventContent(remote, keyEvent); 120 | template.convertAndSend(KeyEventSender.ADDRESS_PREFIX + remote.userID, keyEvent); 121 | 122 | return "Connect Game Success!"; 123 | 124 | } else if (connectionQueue.isEmpty() && !IDMapper.containsKey(mapper)) { 125 | 126 | if (!IDMapper.containsKey(mapper)) 127 | connectionQueue.offer(mapper); 128 | return "You've joined the game, waiting For Remote Connection!"; 129 | 130 | } else { 131 | disConnectGame(mapper, keyEvent); 132 | return connect(keyEvent); 133 | } 134 | } 135 | 136 | } 137 | 138 | 139 | public String getSendURL(String IPAddress, String userID) throws Exception { 140 | 141 | URLMapper urlMapper = new URLMapper(IPAddress, userID); 142 | 143 | if (!IDMapper.containsKey(urlMapper)) throw new Exception("URL doesn't match!"); 144 | 145 | String sendURL; 146 | sendURL = KeyEventSender.ADDRESS_PREFIX + IDMapper.get(urlMapper).userID; 147 | return sendURL; 148 | 149 | } 150 | 151 | private void changEventContent(URLMapper mapper, KeyEvent event) { 152 | event.setUserID(mapper.userID); 153 | event.setIPAddress(mapper.IPAddress); 154 | } 155 | 156 | 157 | } 158 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >&- 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >&- 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /src/main/java/org/decaywood/KeyEvent.java: -------------------------------------------------------------------------------- 1 | package org.decaywood; 2 | 3 | /** 4 | * @author: decaywood 5 | * @date: 2015/6/15 20:36 6 | */ 7 | public class KeyEvent { 8 | 9 | 10 | private String gameState; 11 | 12 | private String userID; 13 | private String highestScore; 14 | private String altKey; 15 | private String ctrlKey; 16 | private String metaKey; 17 | private String shiftKey; 18 | private String which; 19 | private String currentNum; 20 | private String expectNum; 21 | 22 | private String IPAddress; 23 | private String randomTiles; 24 | 25 | //==================== user reference ================ 26 | 27 | private String userDatabaseID; 28 | private String userLoginName; 29 | private String userEmail; 30 | 31 | 32 | 33 | public boolean canBuffered() { 34 | return currentNum != null && expectNum != null; 35 | } 36 | 37 | public String getUserDatabaseID() { 38 | return userDatabaseID; 39 | } 40 | 41 | public void setUserDatabaseID(String userDatabaseID) { 42 | this.userDatabaseID = userDatabaseID; 43 | } 44 | 45 | public String getUserLoginName() { 46 | return userLoginName; 47 | } 48 | 49 | public void setUserLoginName(String userLoginName) { 50 | this.userLoginName = userLoginName; 51 | } 52 | 53 | public String getUserEmail() { 54 | return userEmail; 55 | } 56 | 57 | public void setUserEmail(String userEmail) { 58 | this.userEmail = userEmail; 59 | } 60 | 61 | public String getRandomTiles() { 62 | return randomTiles; 63 | } 64 | 65 | public void setRandomTiles(String randomTiles) { 66 | this.randomTiles = randomTiles; 67 | } 68 | 69 | public String getGameState() { 70 | return gameState; 71 | } 72 | 73 | public void setGameState(String gameState) { 74 | this.gameState = gameState; 75 | } 76 | 77 | public int getExpectNum() { 78 | return expectNum == null ? 0 : Integer.parseInt(expectNum); 79 | } 80 | 81 | public void setExpectNum(String expectNum) { 82 | this.expectNum = expectNum; 83 | } 84 | 85 | public int getCurrentNum() { 86 | return currentNum == null ? 0 : Integer.parseInt(currentNum); 87 | } 88 | 89 | public void setCurrentNum(String currentNum) { 90 | this.currentNum = currentNum; 91 | } 92 | 93 | public String getIPAddress() { 94 | return IPAddress; 95 | } 96 | 97 | public void setIPAddress(String IPAddress) { 98 | this.IPAddress = IPAddress; 99 | } 100 | 101 | public String getUserID() { 102 | return userID; 103 | } 104 | 105 | public void setUserID(String userID) { 106 | this.userID = userID; 107 | } 108 | 109 | 110 | 111 | public String getHighestScore() { 112 | return highestScore; 113 | } 114 | 115 | public void setHighestScore(String highestScore) { 116 | this.highestScore = highestScore; 117 | } 118 | 119 | public String getWhich() { 120 | return which; 121 | } 122 | 123 | public void setWhich(String which) { 124 | this.which = which; 125 | } 126 | 127 | public String getShiftKey() { 128 | return shiftKey; 129 | } 130 | 131 | public void setShiftKey(String shiftKey) { 132 | this.shiftKey = shiftKey; 133 | } 134 | 135 | public String getMetaKey() { 136 | return metaKey; 137 | } 138 | 139 | public void setMetaKey(String metaKey) { 140 | this.metaKey = metaKey; 141 | } 142 | 143 | public String getCtrlKey() { 144 | return ctrlKey; 145 | } 146 | 147 | public void setCtrlKey(String ctrlKey) { 148 | this.ctrlKey = ctrlKey; 149 | } 150 | 151 | public String getAltKey() { 152 | return altKey; 153 | } 154 | 155 | public void setAltKey(String altKey) { 156 | this.altKey = altKey; 157 | } 158 | 159 | public void copyOf(KeyEvent keyEvent) { 160 | this.gameState = keyEvent.gameState; 161 | 162 | this.userID = keyEvent.userID; 163 | this.highestScore = keyEvent.highestScore; 164 | this.altKey = keyEvent.altKey; 165 | this.ctrlKey = keyEvent.ctrlKey; 166 | this.metaKey = keyEvent.metaKey; 167 | this.shiftKey = keyEvent.shiftKey; 168 | this.which = keyEvent.which; 169 | this.currentNum = keyEvent.currentNum; 170 | this.expectNum = keyEvent.expectNum; 171 | 172 | this.IPAddress = keyEvent.IPAddress; 173 | this.randomTiles = keyEvent.randomTiles; 174 | 175 | this.userEmail = keyEvent.userEmail; 176 | this.userDatabaseID = keyEvent.userDatabaseID; 177 | this.userLoginName = keyEvent.userLoginName; 178 | } 179 | 180 | @Override 181 | public String toString() { 182 | return "KeyEvent{" + 183 | "gameState='" + gameState + '\'' + 184 | ", userID='" + userID + '\'' + 185 | ", highestScore='" + highestScore + '\'' + 186 | ", altKey='" + altKey + '\'' + 187 | ", ctrlKey='" + ctrlKey + '\'' + 188 | ", metaKey='" + metaKey + '\'' + 189 | ", shiftKey='" + shiftKey + '\'' + 190 | ", which='" + which + '\'' + 191 | ", currentNum='" + currentNum + '\'' + 192 | ", expectNum='" + expectNum + '\'' + 193 | ", IPAddress='" + IPAddress + '\'' + 194 | ", randomTiles='" + randomTiles + '\'' + 195 | ", userDatabaseID='" + userDatabaseID + '\'' + 196 | ", userLoginName='" + userLoginName + '\'' + 197 | ", userEmail='" + userEmail + '\'' + 198 | '}'; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/main/java/org/decaywood/KeyEventSequencer.java: -------------------------------------------------------------------------------- 1 | package org.decaywood; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | import java.util.LinkedList; 6 | import java.util.Queue; 7 | import java.util.concurrent.ConcurrentHashMap; 8 | import java.util.function.Consumer; 9 | 10 | /** 11 | * @author: decaywood 12 | * @date: 2015/7/7 22:43. 13 | * 14 | * KeyEventSequencer is used to ensure the sending order of keyEvent 15 | * according to keyEvent currentNum and expectedNum, it is easy to ensure 16 | * the order. for example if a keyEvent in which currentNum is 1 and expectedNum is 2 17 | * but the coming keyEvent in which currentNum is 3, the No3 keyEvent would be cached 18 | * in sequencer and waiting for No2, when No2 is coming, No2 and No3 will be send at 19 | * the same time. 20 | * 21 | */ 22 | 23 | @Component("KeyEventSequencer") 24 | public class KeyEventSequencer { 25 | 26 | private static class BufferKey { 27 | 28 | String IPAddress; 29 | boolean marker; 30 | final String userID; 31 | final int key; 32 | 33 | public BufferKey(String IPAddress, String userID, int key) { 34 | this.IPAddress = IPAddress; 35 | this.userID = userID; 36 | this.key = key; 37 | } 38 | 39 | BufferKey mark() { 40 | this.marker = true; 41 | return this; 42 | } 43 | 44 | BufferKey unMark() { 45 | this.marker = false; 46 | return this; 47 | } 48 | 49 | 50 | @Override 51 | public boolean equals(Object o) { 52 | if (this == o) return true; 53 | if (o == null || getClass() != o.getClass()) return false; 54 | 55 | BufferKey bufferKey = (BufferKey) o; 56 | 57 | if (marker != bufferKey.marker) return false; 58 | else if (key != bufferKey.key) return false; 59 | else if (!IPAddress.equals(bufferKey.IPAddress)) return false; 60 | return userID.equals(bufferKey.userID); 61 | 62 | } 63 | 64 | @Override 65 | public int hashCode() { 66 | int result = IPAddress.hashCode(); 67 | result = 31 * result + (marker ? 1 : 0); 68 | result = 31 * result + userID.hashCode(); 69 | result = 31 * result + key; 70 | return result; 71 | } 72 | } 73 | 74 | /** 75 | * this queue is just for collect the keyEvents, 76 | * it is for reusing the queue rather than new a object when execute the method 77 | * every time so that it can reduce garbage recycle 78 | * it is thread safety 79 | */ 80 | private ThreadLocal> eventCollector = ThreadLocal.withInitial(LinkedList::new); 81 | 82 | 83 | 84 | private ConcurrentHashMap keyEventBuffer; 85 | 86 | public KeyEventSequencer() { 87 | this.keyEventBuffer = new ConcurrentHashMap<>(1 << 10); 88 | } 89 | 90 | public synchronized void processKeyEvent(KeyEvent keyEvent, Consumer operator) { 91 | 92 | 93 | if(!keyEvent.canBuffered()){ 94 | operator.accept(keyEvent); 95 | return; 96 | } 97 | 98 | BufferKey markKey = getBufferKey(keyEvent.getIPAddress(), keyEvent.getUserID(), 0).mark(); 99 | if (!keyEventBuffer.containsKey(markKey)) { 100 | keyEventBuffer.put(markKey, keyEvent); 101 | clearUserData(keyEvent.getIPAddress(), keyEvent.getUserID()); 102 | } 103 | 104 | Queue queue = eventCollector.get(); 105 | 106 | BufferKey bufferKey = getBufferKey(keyEvent.getIPAddress(), keyEvent.getUserID(), keyEvent.getCurrentNum()).mark(); 107 | 108 | if (keyEventBuffer.containsKey(bufferKey)) { 109 | 110 | collectKeyEvent(queue, keyEvent); 111 | if(keyEvent.getCurrentNum() != 0) 112 | keyEventBuffer.remove(bufferKey); 113 | 114 | } else { 115 | bufferKey.unMark(); 116 | keyEventBuffer.put(bufferKey, keyEvent); 117 | 118 | } 119 | 120 | sendEvent(queue, operator); 121 | 122 | } 123 | 124 | public void clearUserData(String IPAddress, String userID) { 125 | 126 | keyEventBuffer.forEachKey(Integer.MAX_VALUE, bufferKey -> { 127 | 128 | if (!bufferKey.IPAddress.equalsIgnoreCase(IPAddress)) return; 129 | if (bufferKey.userID.equalsIgnoreCase(userID)) return; 130 | keyEventBuffer.remove(bufferKey); 131 | 132 | }); 133 | 134 | } 135 | 136 | 137 | private BufferKey getBufferKey(String IPAddress, String userID, int key) { 138 | 139 | return new BufferKey(IPAddress, userID, key); 140 | 141 | } 142 | 143 | private void collectKeyEvent(Queue queue, KeyEvent event) { 144 | 145 | queue.offer(event); 146 | String ip = event.getIPAddress(); 147 | String userID = event.getUserID(); 148 | int expectNum = event.getExpectNum(); 149 | BufferKey bufferKey = getBufferKey(ip, userID, expectNum); 150 | 151 | KeyEvent nextEvent = null; 152 | 153 | while (keyEventBuffer.containsKey(bufferKey)) { 154 | 155 | nextEvent = keyEventBuffer.get(bufferKey); 156 | queue.offer(nextEvent); 157 | expectNum = nextEvent.getExpectNum(); 158 | keyEventBuffer.remove(bufferKey); 159 | bufferKey = getBufferKey(ip, userID, expectNum); 160 | 161 | } 162 | 163 | bufferKey.mark(); 164 | keyEventBuffer.put(bufferKey, nextEvent == null ? event : nextEvent); 165 | 166 | } 167 | 168 | private void sendEvent(Queue queue, Consumer operator) { 169 | while (!queue.isEmpty()) { 170 | KeyEvent event = queue.poll(); 171 | operator.accept(event); 172 | } 173 | } 174 | 175 | 176 | } 177 | -------------------------------------------------------------------------------- /src/main/resources/static/2048.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 2048 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |
24 |
25 |

HOME

26 |
27 |
0
28 |
0
29 |
30 |
31 | 32 |
33 |

Join the numbers and get to the 2048 tile!

34 | Connect 35 |
36 | 37 |
38 |
39 |

40 |
41 | Keep going 42 | Try again 43 |
44 |
45 | 46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | 73 |
74 | 75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |

GUEST

83 |
84 |
0
85 |
0
86 |
87 |
88 | 89 |
90 |

91 |
92 | 93 |
94 |
95 |

96 |
97 | Keep going 98 | Try again 99 |
100 |
101 | 102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 | 129 |
130 | 131 |
132 |
133 | 134 | 135 | 136 |
137 |
138 | 139 |
140 | 141 |
142 | 145 |
146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /src/main/resources/static/js/net_send_manager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by decaywood on 2015/7/2. 3 | */ 4 | 5 | function NetSendManager(gameManager, remoteManager) { 6 | 7 | this.msg = $('#msg'); 8 | this.gameManager = gameManager; 9 | this.remoteManager = remoteManager; 10 | this.userID = Math.uuidCompact(); 11 | var sock= new SockJS('/webSocket'); 12 | this.stompClient = Stomp.over(sock); 13 | 14 | this.counter = 0; 15 | 16 | this.innerMapper = initInnerMapper(); 17 | 18 | 19 | this.map = { 20 | 38: 0, // Up 21 | 39: 1, // Right 22 | 40: 2, // Down 23 | 37: 3, // Left 24 | 75: 0, // Vim up 25 | 76: 1, // Vim right 26 | 74: 2, // Vim down 27 | 72: 3, // Vim left 28 | 87: 0, // W 29 | 68: 1, // D 30 | 83: 2, // S 31 | 65: 3 // A 32 | }; 33 | this.initStomp(); 34 | } 35 | 36 | 37 | 38 | NetSendManager.prototype.initStomp = function () { 39 | 40 | var that = this; 41 | 42 | var callback = function () { 43 | console.info("in call back") 44 | that.stompClient.subscribe('/message/responds/' + that.userID, function(responds){ 45 | 46 | var tuple = getElement(responds); 47 | var mapped = that.map[tuple.keyEvent.which]; 48 | 49 | console.info(tuple.gameState); 50 | if(tuple.gameState == "game_connect_ack") { 51 | that.initGame(that.sendData, that.gameManager, that.userID); 52 | } 53 | 54 | else if(tuple.gameState == "game_init") 55 | that.remoteManager.restart(tuple.tiles, tuple.highestScore, tuple.IP); 56 | 57 | else if(tuple.gameState == "gaming" && mapped !== undefined) { 58 | 59 | var entry = tuple.tiles.pop(); 60 | entry.move = mapped; 61 | var tileArr = that.innerMapper.getTile(tuple.currentNum, entry); 62 | 63 | while(tileArr.length != 0) { 64 | var t = tileArr.shift(); 65 | that.remoteManager.move(t.move, t, tuple.highestScore); 66 | } 67 | } 68 | 69 | else if (tuple.gameState == "game_disconnect") { 70 | that.sendMsg("Game Reconnect"); 71 | that.gameManager.clearGame(); 72 | that.remoteManager.clearGame(); 73 | that.resetState(); 74 | } 75 | 76 | }); 77 | }; 78 | var errorCallback = function (e) { 79 | console.info(e); 80 | }; 81 | this.stompClient.connect("","", callback,errorCallback); 82 | }; 83 | 84 | NetSendManager.prototype.sendGameState = function (keyEvent) { 85 | 86 | var tiles = this.gameManager.getRandomTiles(); 87 | var bestScore = this.gameManager.getBestScore(); 88 | 89 | var which = keyEvent.which; 90 | var map = this.map; 91 | var mapped = map[which]; 92 | 93 | if(mapped !== undefined) { 94 | var currentNum = this.counter++; 95 | var expectNum = this.counter; 96 | } 97 | 98 | var message = { 99 | userID:this.userID, 100 | gameState:"gaming", 101 | highestScore:bestScore, 102 | altKey:keyEvent.altKey ? keyEvent.altKey : "", 103 | ctrlKey:keyEvent.ctrlKey ? keyEvent.ctrlKey : "", 104 | metaKey:keyEvent.metaKey ? keyEvent.metaKey : "", 105 | shiftKey:keyEvent.shiftKey ? keyEvent.shiftKey : "", 106 | which:keyEvent.which ? keyEvent.which : "", 107 | currentNum:currentNum, 108 | expectNum:expectNum, 109 | randomTiles:JSON.stringify(tiles) 110 | }; 111 | 112 | this.sendData('keyDown', message); 113 | 114 | }; 115 | 116 | NetSendManager.prototype.initGame = function (sender, gameManager, userID) { 117 | 118 | gameManager.restart(); 119 | var tiles = gameManager.getRandomTiles(); 120 | var bestScore = gameManager.getBestScore(); 121 | 122 | 123 | var message = { 124 | userID:userID, 125 | gameState:"game_init", 126 | highestScore:bestScore, 127 | randomTiles:JSON.stringify(tiles) 128 | }; 129 | 130 | sender('keyDown', message); 131 | 132 | 133 | }; 134 | 135 | NetSendManager.prototype.sendMsg = function (info) { 136 | var msg = this.msg; 137 | msg.text(info); 138 | setTimeout(function () {msg.text(' ');},3000) 139 | }; 140 | 141 | NetSendManager.prototype.connectGame = function () { 142 | this.resetState(); 143 | var that = this; 144 | var sender = this.sendData; 145 | var success = function (info) { 146 | that.sendMsg(info); 147 | }; 148 | sender('connectGame', { 149 | userID:that.userID, 150 | gameState:"game_connect" 151 | }, success) 152 | 153 | }; 154 | 155 | NetSendManager.prototype.sendData = function (target, message, success) { 156 | $.ajax({ 157 | url:target, 158 | data:message, 159 | async:true, 160 | cache:false, 161 | type:'POST', 162 | dataType:'json', 163 | success:success, 164 | error: function (e) { 165 | if(e.responseText) { 166 | success(e.responseText); // non-Json solution 167 | } 168 | } 169 | }); 170 | }; 171 | 172 | NetSendManager.prototype.resetState = function () { 173 | 174 | this.innerMapper.reset(); 175 | this.counter = 0; 176 | 177 | }; 178 | 179 | var getElement = function (jsonFile) { 180 | 181 | var jsonString = JSON.stringify(jsonFile); 182 | 183 | var event = JSON.parse(jsonString).body; 184 | var body = JSON.parse(event); 185 | 186 | var keyEvent = eventParser(body); 187 | var tiles = tilesParser(body); 188 | var gameState = body.gameState; 189 | var highestScore = body.highestScore; 190 | var currentNum = body.currentNum; 191 | 192 | return { 193 | keyEvent:keyEvent, 194 | tiles:tiles, 195 | gameState:gameState, 196 | highestScore:highestScore, 197 | currentNum:currentNum, 198 | IP:body.ipaddress 199 | }; 200 | 201 | }; 202 | 203 | var eventParser = function (jsonFile) { 204 | return { 205 | altKey:jsonFile.altKey, 206 | ctrlKey:jsonFile.ctrlKey, 207 | metaKey:jsonFile.metaKey, 208 | shiftKey:jsonFile.shiftKey, 209 | which:jsonFile.which 210 | }; 211 | }; 212 | 213 | var tilesParser = function (jsonFile) { 214 | var randomTiles = jsonFile.randomTiles; 215 | return JSON.parse(randomTiles); 216 | }; 217 | 218 | var initInnerMapper = function () { 219 | return { 220 | mapper:{}, 221 | stackTracer:0, 222 | add: function (currentNum, tile) { 223 | this.mapper[currentNum] = tile; 224 | }, 225 | isValid: function (currentNum) { 226 | return this.mapper.hasOwnProperty(currentNum); 227 | }, 228 | getTile: function (currentNum, tile) { 229 | 230 | if(currentNum == this.stackTracer) { 231 | var array = [tile]; 232 | this.stackTracer++; 233 | var index = JSON.stringify(this.stackTracer); 234 | while(this.isValid(index)){ 235 | array.push(this.mapper[index]); 236 | delete this.mapper[index]; 237 | index = JSON.stringify(this.stackTracer++); 238 | } 239 | return array; 240 | } else { 241 | this.mapper[currentNum] = tile; 242 | return []; 243 | } 244 | }, 245 | reset: function () { 246 | this.mapper = {}; 247 | this.stackTracer = 0; 248 | } 249 | }; 250 | }; 251 | 252 | -------------------------------------------------------------------------------- /src/main/resources/static/js/remote_game_manager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by decaywood on 2015/7/2. 3 | */ 4 | function RemoteGameManager(size, Actuator, target) { 5 | this.size = size; // Size of the grid 6 | this.actuator = new Actuator(target); 7 | this.startTiles = 2; 8 | this.updateIPAddress(""); 9 | } 10 | 11 | 12 | RemoteGameManager.prototype.clearGame = function () { 13 | this.setup(); 14 | }; 15 | 16 | // Restart the game 17 | RemoteGameManager.prototype.restart = function (tiles, bestScore, IPAddress) { 18 | this.setup(tiles, bestScore); 19 | this.updateIPAddress(IPAddress); 20 | }; 21 | 22 | RemoteGameManager.prototype.updateIPAddress = function (IPAddress) { 23 | this.actuator.updateIPAddress(IPAddress); 24 | }; 25 | 26 | // Keep playing after winning (allows going over 2048) 27 | RemoteGameManager.prototype.keepPlaying = function () { 28 | this.keepPlaying = true; 29 | this.actuator.continueGame(); // Clear the game won/lost message 30 | }; 31 | 32 | // Return true if the game is lost, or has won and the user hasn't kept playing 33 | RemoteGameManager.prototype.isGameTerminated = function () { 34 | return this.over || (this.won && !this.keepPlaying); 35 | }; 36 | 37 | // Set up the game 38 | RemoteGameManager.prototype.setup = function (randomTiles, bestScore) { 39 | 40 | this.actuator.continueGame(); // Clear the game won/lost message 41 | 42 | var score = bestScore == undefined ? 0 : bestScore; 43 | 44 | this.grid = new Grid(this.size); 45 | this.score = 0; 46 | this.bestScore = score; 47 | this.over = false; 48 | this.won = false; 49 | this.keepPlaying = false; 50 | 51 | // Add the initial tiles 52 | if(randomTiles != undefined) 53 | this.addStartTiles(randomTiles); 54 | 55 | 56 | // Update the actuator 57 | this.actuate(); 58 | }; 59 | 60 | // Set up the initial tiles to start the game with 61 | RemoteGameManager.prototype.addStartTiles = function (randomTiles) { 62 | for (var i = 0; i < randomTiles.length; i++) { 63 | var randomTile = randomTiles[i]; 64 | var x = randomTile.x; 65 | var y = randomTile.y; 66 | var position = {x: x, y: y}; 67 | var tile = new Tile(position, randomTile.value); 68 | this.addRandomTile(tile); 69 | } 70 | }; 71 | 72 | // Adds a tile in a random position 73 | RemoteGameManager.prototype.addRandomTile = function (tile) { 74 | if (this.grid.cellsAvailable()) { 75 | this.grid.insertTile(tile); 76 | } 77 | }; 78 | 79 | // Sends the updated grid to the actuator 80 | RemoteGameManager.prototype.actuate = function () { 81 | 82 | if (this.bestScore < this.score) { 83 | this.bestScore = score; 84 | } 85 | 86 | this.actuator.actuate(this.grid, { 87 | score: this.score, 88 | over: this.over, 89 | won: this.won, 90 | bestScore: this.bestScore, 91 | terminated: this.isGameTerminated() 92 | }); 93 | 94 | }; 95 | 96 | // Represent the current game as an object 97 | RemoteGameManager.prototype.serialize = function () { 98 | return { 99 | grid: this.grid.serialize(), 100 | score: this.score, 101 | over: this.over, 102 | won: this.won, 103 | keepPlaying: this.keepPlaying 104 | }; 105 | }; 106 | 107 | // Save all tile positions and remove merger info 108 | RemoteGameManager.prototype.prepareTiles = function () { 109 | this.grid.eachCell(function (x, y, tile) { 110 | if (tile) { 111 | tile.mergedFrom = null; 112 | tile.savePosition(); 113 | } 114 | }); 115 | }; 116 | 117 | // Move a tile and its representation 118 | RemoteGameManager.prototype.moveTile = function (tile, cell) { 119 | this.grid.cells[tile.x][tile.y] = null; 120 | this.grid.cells[cell.x][cell.y] = tile; 121 | tile.updatePosition(cell); 122 | }; 123 | 124 | // Move tiles on the grid in the specified direction 125 | RemoteGameManager.prototype.move = function (direction, randomTile, bestScore) { 126 | 127 | this.bestScore = bestScore; 128 | // 0: up, 1: right, 2: down, 3: left 129 | var self = this; 130 | 131 | if (this.isGameTerminated()) return; // Don't do anything if the game's over 132 | 133 | var cell, tile; 134 | 135 | var vector = this.getVector(direction); 136 | var traversals = this.buildTraversals(vector); 137 | var moved = false; 138 | 139 | // Save the current tile positions and remove merger information 140 | this.prepareTiles(); 141 | 142 | // Traverse the grid in the right direction and move tiles 143 | traversals.x.forEach(function (x) { 144 | traversals.y.forEach(function (y) { 145 | cell = { x: x, y: y }; 146 | tile = self.grid.cellContent(cell); 147 | 148 | if (tile) { 149 | var positions = self.findFarthestPosition(cell, vector); 150 | var next = self.grid.cellContent(positions.next); 151 | 152 | // Only one merger per row traversal? 153 | if (next && next.value === tile.value && !next.mergedFrom) { 154 | var merged = new Tile(positions.next, tile.value * 2); 155 | merged.mergedFrom = [tile, next]; 156 | 157 | self.grid.insertTile(merged); 158 | self.grid.removeTile(tile); 159 | 160 | // Converge the two tiles' positions 161 | tile.updatePosition(positions.next); 162 | 163 | // Update the score 164 | self.score += merged.value; 165 | 166 | // The mighty 2048 tile 167 | if (merged.value === 2048) self.won = true; 168 | } else { 169 | self.moveTile(tile, positions.farthest); 170 | } 171 | 172 | if (!self.positionsEqual(cell, tile)) { 173 | moved = true; // The tile moved from its original cell! 174 | } 175 | } 176 | }); 177 | }); 178 | 179 | if (moved) { 180 | var x = randomTile.x; 181 | var y = randomTile.y; 182 | var position = {x: x, y: y}; 183 | var t = new Tile(position, randomTile.value); 184 | 185 | this.addRandomTile(t); 186 | 187 | if (!this.movesAvailable()) { 188 | this.over = true; // Game over! 189 | } 190 | 191 | this.actuate(); 192 | } 193 | }; 194 | 195 | // Get the vector representing the chosen direction 196 | RemoteGameManager.prototype.getVector = function (direction) { 197 | // Vectors representing tile movement 198 | var map = { 199 | 0: { x: 0, y: -1 }, // Up 200 | 1: { x: 1, y: 0 }, // Right 201 | 2: { x: 0, y: 1 }, // Down 202 | 3: { x: -1, y: 0 } // Left 203 | }; 204 | 205 | return map[direction]; 206 | }; 207 | 208 | // Build a list of positions to traverse in the right order 209 | RemoteGameManager.prototype.buildTraversals = function (vector) { 210 | var traversals = { x: [], y: [] }; 211 | 212 | for (var pos = 0; pos < this.size; pos++) { 213 | traversals.x.push(pos); 214 | traversals.y.push(pos); 215 | } 216 | 217 | // Always traverse from the farthest cell in the chosen direction 218 | if (vector.x === 1) traversals.x = traversals.x.reverse(); 219 | if (vector.y === 1) traversals.y = traversals.y.reverse(); 220 | 221 | return traversals; 222 | }; 223 | 224 | RemoteGameManager.prototype.findFarthestPosition = function (cell, vector) { 225 | var previous; 226 | 227 | // Progress towards the vector direction until an obstacle is found 228 | do { 229 | previous = cell; 230 | cell = { x: previous.x + vector.x, y: previous.y + vector.y }; 231 | } while (this.grid.withinBounds(cell) && 232 | this.grid.cellAvailable(cell)); 233 | 234 | return { 235 | farthest: previous, 236 | next: cell // Used to check if a merge is required 237 | }; 238 | }; 239 | 240 | RemoteGameManager.prototype.movesAvailable = function () { 241 | return this.grid.cellsAvailable() || this.tileMatchesAvailable(); 242 | }; 243 | 244 | // Check for available matches between tiles (more expensive check) 245 | RemoteGameManager.prototype.tileMatchesAvailable = function () { 246 | var self = this; 247 | 248 | var tile; 249 | 250 | for (var x = 0; x < this.size; x++) { 251 | for (var y = 0; y < this.size; y++) { 252 | tile = this.grid.cellContent({ x: x, y: y }); 253 | 254 | if (tile) { 255 | for (var direction = 0; direction < 4; direction++) { 256 | var vector = self.getVector(direction); 257 | var cell = { x: x + vector.x, y: y + vector.y }; 258 | 259 | var other = self.grid.cellContent(cell); 260 | 261 | if (other && other.value === tile.value) { 262 | return true; // These two tiles can be merged 263 | } 264 | } 265 | } 266 | } 267 | } 268 | 269 | return false; 270 | }; 271 | 272 | RemoteGameManager.prototype.positionsEqual = function (first, second) { 273 | return first.x === second.x && first.y === second.y; 274 | }; 275 | -------------------------------------------------------------------------------- /src/main/resources/static/js/game_manager.js: -------------------------------------------------------------------------------- 1 | function GameManager(size, InputManager, Actuator, StorageManager, NetSendManager, target) { 2 | this.size = size; // Size of the grid 3 | var remoteManager = new RemoteGameManager(4, HTMLActuator, '#guest'); 4 | this.netSendManager = new NetSendManager(this, remoteManager); 5 | this.inputManager = new InputManager(this.netSendManager); 6 | this.storageManager = new StorageManager; 7 | this.actuator = new Actuator(target); 8 | 9 | this.startTiles = 2; 10 | this.randomTiles = new Array(this.startTiles); 11 | 12 | this.inputManager.on("move", this.move.bind(this)); 13 | this.inputManager.on("connect", this.connect.bind(this)); 14 | this.inputManager.on("keepPlaying", this.keepPlaying.bind(this)); 15 | 16 | //this.netSendManager.connectGame(); 17 | 18 | } 19 | 20 | GameManager.prototype.getRandomTiles = function () { 21 | return this.randomTiles; 22 | }; 23 | 24 | // Connect the game 25 | GameManager.prototype.connect = function () { 26 | this.netSendManager.connectGame(); 27 | }; 28 | 29 | GameManager.prototype.getBestScore = function () { 30 | return this.storageManager.getBestScore(); 31 | }; 32 | 33 | GameManager.prototype.clearGame = function () { 34 | this.setup(false); 35 | }; 36 | 37 | GameManager.prototype.restart = function () { 38 | this.setup(true); 39 | }; 40 | 41 | // Keep playing after winning (allows going over 2048) 42 | GameManager.prototype.keepPlaying = function () { 43 | this.keepPlaying = true; 44 | this.actuator.continueGame(); // Clear the game won/lost message 45 | }; 46 | 47 | // Return true if the game is lost, or has won and the user hasn't kept playing 48 | GameManager.prototype.isGameTerminated = function () { 49 | return this.over || (this.won && !this.keepPlaying); 50 | }; 51 | 52 | // Set up the game 53 | GameManager.prototype.setup = function (execute) { 54 | 55 | this.storageManager.clearGameState(); 56 | this.actuator.continueGame(); // Clear the game won/lost message 57 | 58 | var previousState = this.storageManager.getGameState(); 59 | 60 | // Reload the game from a previous game if present 61 | if (previousState) { 62 | this.grid = new Grid(previousState.grid.size, 63 | previousState.grid.cells); // Reload grid 64 | this.score = previousState.score; 65 | this.over = previousState.over; 66 | this.won = previousState.won; 67 | this.keepPlaying = previousState.keepPlaying; 68 | } else { 69 | this.grid = new Grid(this.size); 70 | this.score = 0; 71 | this.over = false; 72 | this.won = false; 73 | this.keepPlaying = false; 74 | 75 | // Add the initial tiles 76 | if(execute) 77 | this.addStartTiles(); 78 | } 79 | 80 | // Update the actuator 81 | this.actuate(); 82 | }; 83 | 84 | // Set up the initial tiles to start the game with 85 | GameManager.prototype.addStartTiles = function () { 86 | this.randomTiles.length = 0; // clear arrays 87 | for (var i = 0; i < this.startTiles; i++) { 88 | this.addRandomTile(); 89 | } 90 | }; 91 | 92 | // Adds a tile in a random position 93 | GameManager.prototype.addRandomTile = function () { 94 | if (this.grid.cellsAvailable()) { 95 | var value = Math.random() < 0.9 ? 2 : 4; 96 | var tile = new Tile(this.grid.randomAvailableCell(), value); 97 | this.randomTiles.push(tile); 98 | this.grid.insertTile(tile); 99 | } 100 | }; 101 | 102 | // Sends the updated grid to the actuator 103 | GameManager.prototype.actuate = function () { 104 | if (this.storageManager.getBestScore() < this.score) { 105 | this.storageManager.setBestScore(this.score); 106 | //this.netSendManager 107 | } 108 | 109 | // Clear the state when the game is over (game over only, not win) 110 | if (this.over) { 111 | this.storageManager.clearGameState(); 112 | } else { 113 | this.storageManager.setGameState(this.serialize()); 114 | } 115 | 116 | this.actuator.actuate(this.grid, { 117 | score: this.score, 118 | over: this.over, 119 | won: this.won, 120 | bestScore: this.storageManager.getBestScore(), 121 | terminated: this.isGameTerminated() 122 | }); 123 | 124 | }; 125 | 126 | // Represent the current game as an object 127 | GameManager.prototype.serialize = function () { 128 | return { 129 | grid: this.grid.serialize(), 130 | score: this.score, 131 | over: this.over, 132 | won: this.won, 133 | keepPlaying: this.keepPlaying 134 | }; 135 | }; 136 | 137 | // Save all tile positions and remove merger info 138 | GameManager.prototype.prepareTiles = function () { 139 | this.grid.eachCell(function (x, y, tile) { 140 | if (tile) { 141 | tile.mergedFrom = null; 142 | tile.savePosition(); 143 | } 144 | }); 145 | }; 146 | 147 | // Move a tile and its representation 148 | GameManager.prototype.moveTile = function (tile, cell) { 149 | this.grid.cells[tile.x][tile.y] = null; 150 | this.grid.cells[cell.x][cell.y] = tile; 151 | tile.updatePosition(cell); 152 | }; 153 | 154 | // Move tiles on the grid in the specified direction 155 | GameManager.prototype.move = function (direction) { 156 | // 0: up, 1: right, 2: down, 3: left 157 | var self = this; 158 | 159 | if (this.isGameTerminated()) return; // Don't do anything if the game's over 160 | 161 | var cell, tile; 162 | 163 | var vector = this.getVector(direction); 164 | var traversals = this.buildTraversals(vector); 165 | var moved = false; 166 | 167 | // Save the current tile positions and remove merger information 168 | this.prepareTiles(); 169 | 170 | // Traverse the grid in the right direction and move tiles 171 | traversals.x.forEach(function (x) { 172 | traversals.y.forEach(function (y) { 173 | cell = { x: x, y: y }; 174 | tile = self.grid.cellContent(cell); 175 | 176 | if (tile) { 177 | var positions = self.findFarthestPosition(cell, vector); 178 | var next = self.grid.cellContent(positions.next); 179 | 180 | // Only one merger per row traversal? 181 | if (next && next.value === tile.value && !next.mergedFrom) { 182 | var merged = new Tile(positions.next, tile.value * 2); 183 | merged.mergedFrom = [tile, next]; 184 | 185 | self.grid.insertTile(merged); 186 | self.grid.removeTile(tile); 187 | 188 | // Converge the two tiles' positions 189 | tile.updatePosition(positions.next); 190 | 191 | // Update the score 192 | self.score += merged.value; 193 | 194 | // The mighty 2048 tile 195 | if (merged.value === 2048) self.won = true; 196 | } else { 197 | self.moveTile(tile, positions.farthest); 198 | } 199 | 200 | if (!self.positionsEqual(cell, tile)) { 201 | moved = true; // The tile moved from its original cell! 202 | } 203 | } 204 | }); 205 | }); 206 | 207 | if (moved) { 208 | this.randomTiles.length = 0; 209 | this.addRandomTile(); 210 | 211 | if (!this.movesAvailable()) { 212 | this.over = true; // Game over! 213 | } 214 | 215 | this.actuate(); 216 | } 217 | }; 218 | 219 | // Get the vector representing the chosen direction 220 | GameManager.prototype.getVector = function (direction) { 221 | // Vectors representing tile movement 222 | var map = { 223 | 0: { x: 0, y: -1 }, // Up 224 | 1: { x: 1, y: 0 }, // Right 225 | 2: { x: 0, y: 1 }, // Down 226 | 3: { x: -1, y: 0 } // Left 227 | }; 228 | 229 | return map[direction]; 230 | }; 231 | 232 | // Build a list of positions to traverse in the right order 233 | GameManager.prototype.buildTraversals = function (vector) { 234 | var traversals = { x: [], y: [] }; 235 | 236 | for (var pos = 0; pos < this.size; pos++) { 237 | traversals.x.push(pos); 238 | traversals.y.push(pos); 239 | } 240 | 241 | // Always traverse from the farthest cell in the chosen direction 242 | if (vector.x === 1) traversals.x = traversals.x.reverse(); 243 | if (vector.y === 1) traversals.y = traversals.y.reverse(); 244 | 245 | return traversals; 246 | }; 247 | 248 | GameManager.prototype.findFarthestPosition = function (cell, vector) { 249 | var previous; 250 | 251 | // Progress towards the vector direction until an obstacle is found 252 | do { 253 | previous = cell; 254 | cell = { x: previous.x + vector.x, y: previous.y + vector.y }; 255 | } while (this.grid.withinBounds(cell) && 256 | this.grid.cellAvailable(cell)); 257 | 258 | return { 259 | farthest: previous, 260 | next: cell // Used to check if a merge is required 261 | }; 262 | }; 263 | 264 | GameManager.prototype.movesAvailable = function () { 265 | return this.grid.cellsAvailable() || this.tileMatchesAvailable(); 266 | }; 267 | 268 | // Check for available matches between tiles (more expensive check) 269 | GameManager.prototype.tileMatchesAvailable = function () { 270 | var self = this; 271 | 272 | var tile; 273 | 274 | for (var x = 0; x < this.size; x++) { 275 | for (var y = 0; y < this.size; y++) { 276 | tile = this.grid.cellContent({ x: x, y: y }); 277 | 278 | if (tile) { 279 | for (var direction = 0; direction < 4; direction++) { 280 | var vector = self.getVector(direction); 281 | var cell = { x: x + vector.x, y: y + vector.y }; 282 | 283 | var other = self.grid.cellContent(cell); 284 | 285 | if (other && other.value === tile.value) { 286 | return true; // These two tiles can be merged 287 | } 288 | } 289 | } 290 | } 291 | } 292 | 293 | return false; 294 | }; 295 | 296 | GameManager.prototype.positionsEqual = function (first, second) { 297 | return first.x === second.x && first.y === second.y; 298 | }; 299 | -------------------------------------------------------------------------------- /src/main/resources/static/css/main.scss: -------------------------------------------------------------------------------- 1 | @import "helpers"; 2 | @import "fonts/clear-sans.css"; 3 | 4 | $field-width: 500px; 5 | $grid-spacing: 15px; 6 | $grid-row-cells: 4; 7 | $tile-size: ($field-width - $grid-spacing * ($grid-row-cells + 1)) / $grid-row-cells; 8 | $tile-border-radius: 3px; 9 | 10 | $mobile-threshold: $field-width + 20px; 11 | 12 | $text-color: #776E65; 13 | $bright-text-color: #f9f6f2; 14 | 15 | $tile-color: #eee4da; 16 | $tile-gold-color: #edc22e; 17 | $tile-gold-glow-color: lighten($tile-gold-color, 15%); 18 | 19 | $game-container-margin-top: 40px; 20 | $game-container-background: #bbada0; 21 | 22 | $transition-speed: 100ms; 23 | 24 | html, body { 25 | margin: 0; 26 | padding: 0; 27 | 28 | background: #faf8ef; 29 | color: $text-color; 30 | font-family: "Clear Sans", "Helvetica Neue", Arial, sans-serif; 31 | font-size: 18px; 32 | } 33 | 34 | body { 35 | margin: 80px 0; 36 | } 37 | 38 | .heading { 39 | @include clearfix; 40 | } 41 | 42 | h1.title { 43 | font-size: 80px; 44 | font-weight: bold; 45 | margin: 0; 46 | display: block; 47 | float: left; 48 | } 49 | 50 | @include keyframes(move-up) { 51 | 0% { 52 | top: 25px; 53 | opacity: 1; 54 | } 55 | 56 | 100% { 57 | top: -50px; 58 | opacity: 0; 59 | } 60 | } 61 | 62 | .scores-container { 63 | float: right; 64 | text-align: right; 65 | } 66 | 67 | .score-container, .best-container { 68 | $height: 25px; 69 | 70 | position: relative; 71 | display: inline-block; 72 | background: $game-container-background; 73 | padding: 15px 25px; 74 | font-size: $height; 75 | height: $height; 76 | line-height: $height + 22px; 77 | font-weight: bold; 78 | border-radius: 3px; 79 | color: white; 80 | margin-top: 8px; 81 | text-align: center; 82 | 83 | &:after { 84 | position: absolute; 85 | width: 100%; 86 | top: 10px; 87 | left: 0; 88 | text-transform: uppercase; 89 | font-size: 13px; 90 | line-height: 13px; 91 | text-align: center; 92 | color: $tile-color; 93 | } 94 | 95 | .score-addition { 96 | position: absolute; 97 | right: 30px; 98 | color: red; 99 | font-size: $height; 100 | line-height: $height; 101 | font-weight: bold; 102 | color: rgba($text-color, .9); 103 | z-index: 100; 104 | @include animation(move-up 600ms ease-in); 105 | @include animation-fill-mode(both); 106 | } 107 | } 108 | 109 | .score-container:after { 110 | content: "Score"; 111 | } 112 | 113 | .best-container:after { 114 | content: "Best" 115 | } 116 | 117 | p { 118 | margin-top: 0; 119 | margin-bottom: 10px; 120 | line-height: 1.65; 121 | } 122 | 123 | a { 124 | color: $text-color; 125 | font-weight: bold; 126 | text-decoration: underline; 127 | cursor: pointer; 128 | } 129 | 130 | strong { 131 | &.important { 132 | text-transform: uppercase; 133 | } 134 | } 135 | 136 | hr { 137 | border: none; 138 | border-bottom: 1px solid lighten($text-color, 40%); 139 | margin-top: 20px; 140 | margin-bottom: 30px; 141 | } 142 | 143 | .container { 144 | width: $field-width; 145 | margin: 0 auto; 146 | } 147 | 148 | @include keyframes(fade-in) { 149 | 0% { 150 | opacity: 0; 151 | } 152 | 153 | 100% { 154 | opacity: 1; 155 | } 156 | } 157 | 158 | // Styles for buttons 159 | @mixin button { 160 | display: inline-block; 161 | background: darken($game-container-background, 20%); 162 | border-radius: 3px; 163 | padding: 0 20px; 164 | text-decoration: none; 165 | color: $bright-text-color; 166 | height: 40px; 167 | line-height: 42px; 168 | } 169 | 170 | // Game field mixin used to render CSS at different width 171 | @mixin game-field { 172 | .game-container { 173 | margin-top: $game-container-margin-top; 174 | position: relative; 175 | padding: $grid-spacing; 176 | 177 | cursor: default; 178 | -webkit-touch-callout: none; 179 | -ms-touch-callout: none; 180 | 181 | -webkit-user-select: none; 182 | -moz-user-select: none; 183 | -ms-user-select: none; 184 | 185 | -ms-touch-action: none; 186 | touch-action: none; 187 | 188 | background: $game-container-background; 189 | border-radius: $tile-border-radius * 2; 190 | width: $field-width; 191 | height: $field-width; 192 | -webkit-box-sizing: border-box; 193 | -moz-box-sizing: border-box; 194 | box-sizing: border-box; 195 | 196 | .game-message { 197 | display: none; 198 | 199 | position: absolute; 200 | top: 0; 201 | right: 0; 202 | bottom: 0; 203 | left: 0; 204 | background: rgba($tile-color, .5); 205 | z-index: 100; 206 | 207 | text-align: center; 208 | 209 | p { 210 | font-size: 60px; 211 | font-weight: bold; 212 | height: 60px; 213 | line-height: 60px; 214 | margin-top: 222px; 215 | // height: $field-width; 216 | // line-height: $field-width; 217 | } 218 | 219 | .lower { 220 | display: block; 221 | margin-top: 59px; 222 | } 223 | 224 | a { 225 | @include button; 226 | margin-left: 9px; 227 | // margin-top: 59px; 228 | 229 | &.keep-playing-button { 230 | display: none; 231 | } 232 | } 233 | 234 | @include animation(fade-in 800ms ease $transition-speed * 12); 235 | @include animation-fill-mode(both); 236 | 237 | &.game-won { 238 | background: rgba($tile-gold-color, .5); 239 | color: $bright-text-color; 240 | 241 | a.keep-playing-button { 242 | display: inline-block; 243 | } 244 | } 245 | 246 | &.game-won, &.game-over { 247 | display: block; 248 | } 249 | } 250 | } 251 | 252 | .grid-container { 253 | position: absolute; 254 | z-index: 1; 255 | } 256 | 257 | .grid-row { 258 | margin-bottom: $grid-spacing; 259 | 260 | &:last-child { 261 | margin-bottom: 0; 262 | } 263 | 264 | &:after { 265 | content: ""; 266 | display: block; 267 | clear: both; 268 | } 269 | } 270 | 271 | .grid-cell { 272 | width: $tile-size; 273 | height: $tile-size; 274 | margin-right: $grid-spacing; 275 | float: left; 276 | 277 | border-radius: $tile-border-radius; 278 | 279 | background: rgba($tile-color, .35); 280 | 281 | &:last-child { 282 | margin-right: 0; 283 | } 284 | } 285 | 286 | .tile-container { 287 | position: absolute; 288 | z-index: 2; 289 | } 290 | 291 | .tile { 292 | &, .tile-inner { 293 | width: ceil($tile-size); 294 | height: ceil($tile-size); 295 | line-height: ceil($tile-size); 296 | } 297 | 298 | // Build position classes 299 | @for $x from 1 through $grid-row-cells { 300 | @for $y from 1 through $grid-row-cells { 301 | &.tile-position-#{$x}-#{$y} { 302 | $xPos: floor(($tile-size + $grid-spacing) * ($x - 1)); 303 | $yPos: floor(($tile-size + $grid-spacing) * ($y - 1)); 304 | @include transform(translate($xPos, $yPos)); 305 | } 306 | } 307 | } 308 | } 309 | } 310 | 311 | // End of game-field mixin 312 | @include game-field; 313 | 314 | .tile { 315 | position: absolute; // Makes transforms relative to the top-left corner 316 | 317 | .tile-inner { 318 | border-radius: $tile-border-radius; 319 | 320 | background: $tile-color; 321 | text-align: center; 322 | font-weight: bold; 323 | z-index: 10; 324 | 325 | font-size: 55px; 326 | } 327 | 328 | // Movement transition 329 | @include transition($transition-speed ease-in-out); 330 | -webkit-transition-property: -webkit-transform; 331 | -moz-transition-property: -moz-transform; 332 | transition-property: transform; 333 | 334 | $base: 2; 335 | $exponent: 1; 336 | $limit: 11; 337 | 338 | // Colors for all 11 states, false = no special color 339 | $special-colors: false false, // 2 340 | false false, // 4 341 | #f78e48 true, // 8 342 | #fc5e2e true, // 16 343 | #ff3333 true, // 32 344 | #ff0000 true, // 64 345 | false true, // 128 346 | false true, // 256 347 | false true, // 512 348 | false true, // 1024 349 | false true; // 2048 350 | 351 | // Build tile colors 352 | @while $exponent <= $limit { 353 | $power: pow($base, $exponent); 354 | 355 | &.tile-#{$power} .tile-inner { 356 | // Calculate base background color 357 | $gold-percent: ($exponent - 1) / ($limit - 1) * 100; 358 | $mixed-background: mix($tile-gold-color, $tile-color, $gold-percent); 359 | 360 | $nth-color: nth($special-colors, $exponent); 361 | 362 | $special-background: nth($nth-color, 1); 363 | $bright-color: nth($nth-color, 2); 364 | 365 | @if $special-background { 366 | $mixed-background: mix($special-background, $mixed-background, 55%); 367 | } 368 | 369 | @if $bright-color { 370 | color: $bright-text-color; 371 | } 372 | 373 | // Set background 374 | background: $mixed-background; 375 | 376 | // Add glow 377 | $glow-opacity: max($exponent - 4, 0) / ($limit - 4); 378 | 379 | @if not $special-background { 380 | box-shadow: 0 0 30px 10px rgba($tile-gold-glow-color, $glow-opacity / 1.8), 381 | inset 0 0 0 1px rgba(white, $glow-opacity / 3); 382 | } 383 | 384 | // Adjust font size for bigger numbers 385 | @if $power >= 100 and $power < 1000 { 386 | font-size: 45px; 387 | 388 | // Media queries placed here to avoid carrying over the rest of the logic 389 | @include smaller($mobile-threshold) { 390 | font-size: 25px; 391 | } 392 | } @else if $power >= 1000 { 393 | font-size: 35px; 394 | 395 | @include smaller($mobile-threshold) { 396 | font-size: 15px; 397 | } 398 | } 399 | } 400 | 401 | $exponent: $exponent + 1; 402 | } 403 | 404 | // Super tiles (above 2048) 405 | &.tile-super .tile-inner { 406 | color: $bright-text-color; 407 | background: mix(#333, $tile-gold-color, 95%); 408 | 409 | font-size: 30px; 410 | 411 | @include smaller($mobile-threshold) { 412 | font-size: 10px; 413 | } 414 | } 415 | } 416 | 417 | @include keyframes(appear) { 418 | 0% { 419 | opacity: 0; 420 | @include transform(scale(0)); 421 | } 422 | 423 | 100% { 424 | opacity: 1; 425 | @include transform(scale(1)); 426 | } 427 | } 428 | 429 | .tile-new .tile-inner { 430 | @include animation(appear 200ms ease $transition-speed); 431 | @include animation-fill-mode(backwards); 432 | } 433 | 434 | @include keyframes(pop) { 435 | 0% { 436 | @include transform(scale(0)); 437 | } 438 | 439 | 50% { 440 | @include transform(scale(1.2)); 441 | } 442 | 443 | 100% { 444 | @include transform(scale(1)); 445 | } 446 | } 447 | 448 | .tile-merged .tile-inner { 449 | z-index: 20; 450 | @include animation(pop 200ms ease $transition-speed); 451 | @include animation-fill-mode(backwards); 452 | } 453 | 454 | .above-game { 455 | @include clearfix; 456 | } 457 | 458 | .game-intro { 459 | float: left; 460 | line-height: 42px; 461 | margin-bottom: 0; 462 | } 463 | 464 | .connect-button { 465 | @include button; 466 | display: block; 467 | text-align: center; 468 | float: right; 469 | } 470 | 471 | .game-explanation { 472 | margin-top: 50px; 473 | } 474 | 475 | @include smaller($mobile-threshold) { 476 | // Redefine variables for smaller screens 477 | $field-width: 280px; 478 | $grid-spacing: 10px; 479 | $grid-row-cells: 4; 480 | $tile-size: ($field-width - $grid-spacing * ($grid-row-cells + 1)) / $grid-row-cells; 481 | $tile-border-radius: 3px; 482 | $game-container-margin-top: 17px; 483 | 484 | html, body { 485 | font-size: 15px; 486 | } 487 | 488 | body { 489 | margin: 20px 0; 490 | padding: 0 20px; 491 | } 492 | 493 | h1.title { 494 | font-size: 27px; 495 | margin-top: 15px; 496 | } 497 | 498 | .container { 499 | width: $field-width; 500 | margin: 0 auto; 501 | } 502 | 503 | .score-container, .best-container { 504 | margin-top: 0; 505 | padding: 15px 10px; 506 | min-width: 40px; 507 | } 508 | 509 | .heading { 510 | margin-bottom: 10px; 511 | } 512 | 513 | // Show intro and restart button side by side 514 | .game-intro { 515 | width: 55%; 516 | display: block; 517 | box-sizing: border-box; 518 | line-height: 1.65; 519 | } 520 | 521 | .connect-button { 522 | width: 42%; 523 | padding: 0; 524 | display: block; 525 | box-sizing: border-box; 526 | margin-top: 2px; 527 | } 528 | 529 | // Render the game field at the right width 530 | @include game-field; 531 | 532 | // Rest of the font-size adjustments in the tile class 533 | .tile .tile-inner { 534 | font-size: 35px; 535 | } 536 | 537 | .game-message { 538 | p { 539 | font-size: 30px !important; 540 | height: 30px !important; 541 | line-height: 30px !important; 542 | margin-top: 90px !important; 543 | } 544 | 545 | .lower { 546 | margin-top: 30px !important; 547 | } 548 | } 549 | } 550 | -------------------------------------------------------------------------------- /src/main/resources/static/css/main.css: -------------------------------------------------------------------------------- 1 | @import url(fonts/clear-sans.css); 2 | 3 | html, body { 4 | margin: 0; 5 | padding: 0; 6 | background: #faf8ef; 7 | color: #776e65; 8 | font-family: "Clear Sans", "Helvetica Neue", Arial, sans-serif; 9 | font-size: 18px; 10 | } 11 | 12 | body { 13 | margin: 80px 0; 14 | } 15 | 16 | .heading:after { 17 | content: ""; 18 | display: block; 19 | clear: both; 20 | } 21 | 22 | h1.title { 23 | font-size: 80px; 24 | font-weight: bold; 25 | margin: 0; 26 | display: block; 27 | float: left; 28 | } 29 | 30 | @-webkit-keyframes move-up { 31 | 0% { 32 | top: 25px; 33 | opacity: 1; 34 | } 35 | 36 | 100% { 37 | top: -50px; 38 | opacity: 0; 39 | } 40 | } 41 | 42 | @-moz-keyframes move-up { 43 | 0% { 44 | top: 25px; 45 | opacity: 1; 46 | } 47 | 48 | 100% { 49 | top: -50px; 50 | opacity: 0; 51 | } 52 | } 53 | 54 | @keyframes move-up { 55 | 0% { 56 | top: 25px; 57 | opacity: 1; 58 | } 59 | 60 | 100% { 61 | top: -50px; 62 | opacity: 0; 63 | } 64 | } 65 | 66 | .scores-container { 67 | float: right; 68 | text-align: right; 69 | } 70 | 71 | .score-container, .best-container { 72 | position: relative; 73 | display: inline-block; 74 | background: #bbada0; 75 | padding: 15px 25px; 76 | font-size: 25px; 77 | height: 25px; 78 | line-height: 47px; 79 | font-weight: bold; 80 | border-radius: 3px; 81 | color: white; 82 | margin-top: 8px; 83 | text-align: center; 84 | } 85 | 86 | .score-container:after, .best-container:after { 87 | position: absolute; 88 | width: 100%; 89 | top: 10px; 90 | left: 0; 91 | text-transform: uppercase; 92 | font-size: 13px; 93 | line-height: 13px; 94 | text-align: center; 95 | color: #eee4da; 96 | } 97 | 98 | .score-container .score-addition, .best-container .score-addition { 99 | position: absolute; 100 | right: 30px; 101 | color: red; 102 | font-size: 25px; 103 | line-height: 25px; 104 | font-weight: bold; 105 | color: rgba(119, 110, 101, 0.9); 106 | z-index: 100; 107 | -webkit-animation: move-up 600ms ease-in; 108 | -moz-animation: move-up 600ms ease-in; 109 | animation: move-up 600ms ease-in; 110 | -webkit-animation-fill-mode: both; 111 | -moz-animation-fill-mode: both; 112 | animation-fill-mode: both; 113 | } 114 | 115 | .score-container:after { 116 | content: "Score"; 117 | } 118 | 119 | .best-container:after { 120 | content: "Best"; 121 | } 122 | 123 | p { 124 | margin-top: 0; 125 | margin-bottom: 10px; 126 | line-height: 1.65; 127 | } 128 | 129 | a { 130 | color: #776e65; 131 | font-weight: bold; 132 | text-decoration: underline; 133 | cursor: pointer; 134 | } 135 | 136 | strong.important { 137 | text-transform: uppercase; 138 | } 139 | 140 | hr { 141 | border: none; 142 | border-bottom: 1px solid #d8d4d0; 143 | margin-top: 20px; 144 | margin-bottom: 30px; 145 | } 146 | 147 | .container { 148 | width: 500px; 149 | margin: 0 auto; 150 | } 151 | 152 | @-webkit-keyframes fade-in { 153 | 0% { 154 | opacity: 0; 155 | } 156 | 157 | 100% { 158 | opacity: 1; 159 | } 160 | } 161 | 162 | @-moz-keyframes fade-in { 163 | 0% { 164 | opacity: 0; 165 | } 166 | 167 | 100% { 168 | opacity: 1; 169 | } 170 | } 171 | 172 | @keyframes fade-in { 173 | 0% { 174 | opacity: 0; 175 | } 176 | 177 | 100% { 178 | opacity: 1; 179 | } 180 | } 181 | 182 | .game-container { 183 | margin-top: 40px; 184 | position: relative; 185 | padding: 15px; 186 | cursor: default; 187 | -webkit-touch-callout: none; 188 | -ms-touch-callout: none; 189 | -webkit-user-select: none; 190 | -moz-user-select: none; 191 | -ms-user-select: none; 192 | -ms-touch-action: none; 193 | touch-action: none; 194 | background: #bbada0; 195 | border-radius: 6px; 196 | width: 500px; 197 | height: 500px; 198 | -webkit-box-sizing: border-box; 199 | -moz-box-sizing: border-box; 200 | box-sizing: border-box; 201 | } 202 | 203 | .game-container .game-message { 204 | display: none; 205 | position: absolute; 206 | top: 0; 207 | right: 0; 208 | bottom: 0; 209 | left: 0; 210 | background: rgba(238, 228, 218, 0.5); 211 | z-index: 100; 212 | text-align: center; 213 | -webkit-animation: fade-in 800ms ease 1200ms; 214 | -moz-animation: fade-in 800ms ease 1200ms; 215 | animation: fade-in 800ms ease 1200ms; 216 | -webkit-animation-fill-mode: both; 217 | -moz-animation-fill-mode: both; 218 | animation-fill-mode: both; 219 | } 220 | 221 | .game-container .game-message p { 222 | font-size: 60px; 223 | font-weight: bold; 224 | height: 60px; 225 | line-height: 60px; 226 | margin-top: 222px; 227 | } 228 | 229 | .game-container .game-message .lower { 230 | display: block; 231 | margin-top: 59px; 232 | } 233 | 234 | .game-container .game-message a { 235 | display: inline-block; 236 | background: #8f7a66; 237 | border-radius: 3px; 238 | padding: 0 20px; 239 | text-decoration: none; 240 | color: #f9f6f2; 241 | height: 40px; 242 | line-height: 42px; 243 | margin-left: 9px; 244 | } 245 | 246 | .game-container .game-message a.keep-playing-button { 247 | display: none; 248 | } 249 | 250 | .game-container .game-message.game-won { 251 | background: rgba(237, 194, 46, 0.5); 252 | color: #f9f6f2; 253 | } 254 | 255 | .game-container .game-message.game-won a.keep-playing-button { 256 | display: inline-block; 257 | } 258 | 259 | .game-container .game-message.game-won, .game-container .game-message.game-over { 260 | display: block; 261 | } 262 | 263 | .grid-container { 264 | position: absolute; 265 | z-index: 1; 266 | } 267 | 268 | .grid-row { 269 | margin-bottom: 15px; 270 | } 271 | 272 | .grid-row:last-child { 273 | margin-bottom: 0; 274 | } 275 | 276 | .grid-row:after { 277 | content: ""; 278 | display: block; 279 | clear: both; 280 | } 281 | 282 | .grid-cell { 283 | width: 106.25px; 284 | height: 106.25px; 285 | margin-right: 15px; 286 | float: left; 287 | border-radius: 3px; 288 | background: rgba(238, 228, 218, 0.35); 289 | } 290 | 291 | .grid-cell:last-child { 292 | margin-right: 0; 293 | } 294 | 295 | .tile-container { 296 | position: absolute; 297 | z-index: 2; 298 | } 299 | 300 | .tile, .tile .tile-inner { 301 | width: 107px; 302 | height: 107px; 303 | line-height: 107px; 304 | } 305 | 306 | .tile.tile-position-1-1 { 307 | -webkit-transform: translate(0px, 0px); 308 | -moz-transform: translate(0px, 0px); 309 | -ms-transform: translate(0px, 0px); 310 | transform: translate(0px, 0px); 311 | } 312 | 313 | .tile.tile-position-1-2 { 314 | -webkit-transform: translate(0px, 121px); 315 | -moz-transform: translate(0px, 121px); 316 | -ms-transform: translate(0px, 121px); 317 | transform: translate(0px, 121px); 318 | } 319 | 320 | .tile.tile-position-1-3 { 321 | -webkit-transform: translate(0px, 242px); 322 | -moz-transform: translate(0px, 242px); 323 | -ms-transform: translate(0px, 242px); 324 | transform: translate(0px, 242px); 325 | } 326 | 327 | .tile.tile-position-1-4 { 328 | -webkit-transform: translate(0px, 363px); 329 | -moz-transform: translate(0px, 363px); 330 | -ms-transform: translate(0px, 363px); 331 | transform: translate(0px, 363px); 332 | } 333 | 334 | .tile.tile-position-2-1 { 335 | -webkit-transform: translate(121px, 0px); 336 | -moz-transform: translate(121px, 0px); 337 | -ms-transform: translate(121px, 0px); 338 | transform: translate(121px, 0px); 339 | } 340 | 341 | .tile.tile-position-2-2 { 342 | -webkit-transform: translate(121px, 121px); 343 | -moz-transform: translate(121px, 121px); 344 | -ms-transform: translate(121px, 121px); 345 | transform: translate(121px, 121px); 346 | } 347 | 348 | .tile.tile-position-2-3 { 349 | -webkit-transform: translate(121px, 242px); 350 | -moz-transform: translate(121px, 242px); 351 | -ms-transform: translate(121px, 242px); 352 | transform: translate(121px, 242px); 353 | } 354 | 355 | .tile.tile-position-2-4 { 356 | -webkit-transform: translate(121px, 363px); 357 | -moz-transform: translate(121px, 363px); 358 | -ms-transform: translate(121px, 363px); 359 | transform: translate(121px, 363px); 360 | } 361 | 362 | .tile.tile-position-3-1 { 363 | -webkit-transform: translate(242px, 0px); 364 | -moz-transform: translate(242px, 0px); 365 | -ms-transform: translate(242px, 0px); 366 | transform: translate(242px, 0px); 367 | } 368 | 369 | .tile.tile-position-3-2 { 370 | -webkit-transform: translate(242px, 121px); 371 | -moz-transform: translate(242px, 121px); 372 | -ms-transform: translate(242px, 121px); 373 | transform: translate(242px, 121px); 374 | } 375 | 376 | .tile.tile-position-3-3 { 377 | -webkit-transform: translate(242px, 242px); 378 | -moz-transform: translate(242px, 242px); 379 | -ms-transform: translate(242px, 242px); 380 | transform: translate(242px, 242px); 381 | } 382 | 383 | .tile.tile-position-3-4 { 384 | -webkit-transform: translate(242px, 363px); 385 | -moz-transform: translate(242px, 363px); 386 | -ms-transform: translate(242px, 363px); 387 | transform: translate(242px, 363px); 388 | } 389 | 390 | .tile.tile-position-4-1 { 391 | -webkit-transform: translate(363px, 0px); 392 | -moz-transform: translate(363px, 0px); 393 | -ms-transform: translate(363px, 0px); 394 | transform: translate(363px, 0px); 395 | } 396 | 397 | .tile.tile-position-4-2 { 398 | -webkit-transform: translate(363px, 121px); 399 | -moz-transform: translate(363px, 121px); 400 | -ms-transform: translate(363px, 121px); 401 | transform: translate(363px, 121px); 402 | } 403 | 404 | .tile.tile-position-4-3 { 405 | -webkit-transform: translate(363px, 242px); 406 | -moz-transform: translate(363px, 242px); 407 | -ms-transform: translate(363px, 242px); 408 | transform: translate(363px, 242px); 409 | } 410 | 411 | .tile.tile-position-4-4 { 412 | -webkit-transform: translate(363px, 363px); 413 | -moz-transform: translate(363px, 363px); 414 | -ms-transform: translate(363px, 363px); 415 | transform: translate(363px, 363px); 416 | } 417 | 418 | .tile { 419 | position: absolute; 420 | -webkit-transition: 100ms ease-in-out; 421 | -moz-transition: 100ms ease-in-out; 422 | transition: 100ms ease-in-out; 423 | -webkit-transition-property: -webkit-transform; 424 | -moz-transition-property: -moz-transform; 425 | transition-property: transform; 426 | } 427 | 428 | .tile .tile-inner { 429 | border-radius: 3px; 430 | background: #eee4da; 431 | text-align: center; 432 | font-weight: bold; 433 | z-index: 10; 434 | font-size: 55px; 435 | } 436 | 437 | .tile.tile-2 .tile-inner { 438 | background: #eee4da; 439 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0), inset 0 0 0 1px rgba(255, 255, 255, 0); 440 | } 441 | 442 | .tile.tile-4 .tile-inner { 443 | background: #ede0c8; 444 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0), inset 0 0 0 1px rgba(255, 255, 255, 0); 445 | } 446 | 447 | .tile.tile-8 .tile-inner { 448 | color: #f9f6f2; 449 | background: #f2b179; 450 | } 451 | 452 | .tile.tile-16 .tile-inner { 453 | color: #f9f6f2; 454 | background: #f59563; 455 | } 456 | 457 | .tile.tile-32 .tile-inner { 458 | color: #f9f6f2; 459 | background: #f67c5f; 460 | } 461 | 462 | .tile.tile-64 .tile-inner { 463 | color: #f9f6f2; 464 | background: #f65e3b; 465 | } 466 | 467 | .tile.tile-128 .tile-inner { 468 | color: #f9f6f2; 469 | background: #edcf72; 470 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.2381), inset 0 0 0 1px rgba(255, 255, 255, 0.14286); 471 | font-size: 45px; 472 | } 473 | 474 | @media screen and (max-width: 520px) { 475 | .tile.tile-128 .tile-inner { 476 | font-size: 25px; 477 | } 478 | } 479 | 480 | .tile.tile-256 .tile-inner { 481 | color: #f9f6f2; 482 | background: #edcc61; 483 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.31746), inset 0 0 0 1px rgba(255, 255, 255, 0.19048); 484 | font-size: 45px; 485 | } 486 | 487 | @media screen and (max-width: 520px) { 488 | .tile.tile-256 .tile-inner { 489 | font-size: 25px; 490 | } 491 | } 492 | 493 | .tile.tile-512 .tile-inner { 494 | color: #f9f6f2; 495 | background: #edc850; 496 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.39683), inset 0 0 0 1px rgba(255, 255, 255, 0.2381); 497 | font-size: 45px; 498 | } 499 | 500 | @media screen and (max-width: 520px) { 501 | .tile.tile-512 .tile-inner { 502 | font-size: 25px; 503 | } 504 | } 505 | 506 | .tile.tile-1024 .tile-inner { 507 | color: #f9f6f2; 508 | background: #edc53f; 509 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.47619), inset 0 0 0 1px rgba(255, 255, 255, 0.28571); 510 | font-size: 35px; 511 | } 512 | 513 | @media screen and (max-width: 520px) { 514 | .tile.tile-1024 .tile-inner { 515 | font-size: 15px; 516 | } 517 | } 518 | 519 | .tile.tile-2048 .tile-inner { 520 | color: #f9f6f2; 521 | background: #edc22e; 522 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.55556), inset 0 0 0 1px rgba(255, 255, 255, 0.33333); 523 | font-size: 35px; 524 | } 525 | 526 | @media screen and (max-width: 520px) { 527 | .tile.tile-2048 .tile-inner { 528 | font-size: 15px; 529 | } 530 | } 531 | 532 | .tile.tile-super .tile-inner { 533 | color: #f9f6f2; 534 | background: #3c3a32; 535 | font-size: 30px; 536 | } 537 | 538 | @media screen and (max-width: 520px) { 539 | .tile.tile-super .tile-inner { 540 | font-size: 10px; 541 | } 542 | } 543 | 544 | @-webkit-keyframes appear { 545 | 0% { 546 | opacity: 0; 547 | -webkit-transform: scale(0); 548 | -moz-transform: scale(0); 549 | -ms-transform: scale(0); 550 | transform: scale(0); 551 | } 552 | 553 | 100% { 554 | opacity: 1; 555 | -webkit-transform: scale(1); 556 | -moz-transform: scale(1); 557 | -ms-transform: scale(1); 558 | transform: scale(1); 559 | } 560 | } 561 | 562 | @-moz-keyframes appear { 563 | 0% { 564 | opacity: 0; 565 | -webkit-transform: scale(0); 566 | -moz-transform: scale(0); 567 | -ms-transform: scale(0); 568 | transform: scale(0); 569 | } 570 | 571 | 100% { 572 | opacity: 1; 573 | -webkit-transform: scale(1); 574 | -moz-transform: scale(1); 575 | -ms-transform: scale(1); 576 | transform: scale(1); 577 | } 578 | } 579 | 580 | @keyframes appear { 581 | 0% { 582 | opacity: 0; 583 | -webkit-transform: scale(0); 584 | -moz-transform: scale(0); 585 | -ms-transform: scale(0); 586 | transform: scale(0); 587 | } 588 | 589 | 100% { 590 | opacity: 1; 591 | -webkit-transform: scale(1); 592 | -moz-transform: scale(1); 593 | -ms-transform: scale(1); 594 | transform: scale(1); 595 | } 596 | } 597 | 598 | .tile-new .tile-inner { 599 | -webkit-animation: appear 200ms ease 100ms; 600 | -moz-animation: appear 200ms ease 100ms; 601 | animation: appear 200ms ease 100ms; 602 | -webkit-animation-fill-mode: backwards; 603 | -moz-animation-fill-mode: backwards; 604 | animation-fill-mode: backwards; 605 | } 606 | 607 | @-webkit-keyframes pop { 608 | 0% { 609 | -webkit-transform: scale(0); 610 | -moz-transform: scale(0); 611 | -ms-transform: scale(0); 612 | transform: scale(0); 613 | } 614 | 615 | 50% { 616 | -webkit-transform: scale(1.2); 617 | -moz-transform: scale(1.2); 618 | -ms-transform: scale(1.2); 619 | transform: scale(1.2); 620 | } 621 | 622 | 100% { 623 | -webkit-transform: scale(1); 624 | -moz-transform: scale(1); 625 | -ms-transform: scale(1); 626 | transform: scale(1); 627 | } 628 | } 629 | 630 | @-moz-keyframes pop { 631 | 0% { 632 | -webkit-transform: scale(0); 633 | -moz-transform: scale(0); 634 | -ms-transform: scale(0); 635 | transform: scale(0); 636 | } 637 | 638 | 50% { 639 | -webkit-transform: scale(1.2); 640 | -moz-transform: scale(1.2); 641 | -ms-transform: scale(1.2); 642 | transform: scale(1.2); 643 | } 644 | 645 | 100% { 646 | -webkit-transform: scale(1); 647 | -moz-transform: scale(1); 648 | -ms-transform: scale(1); 649 | transform: scale(1); 650 | } 651 | } 652 | 653 | @keyframes pop { 654 | 0% { 655 | -webkit-transform: scale(0); 656 | -moz-transform: scale(0); 657 | -ms-transform: scale(0); 658 | transform: scale(0); 659 | } 660 | 661 | 50% { 662 | -webkit-transform: scale(1.2); 663 | -moz-transform: scale(1.2); 664 | -ms-transform: scale(1.2); 665 | transform: scale(1.2); 666 | } 667 | 668 | 100% { 669 | -webkit-transform: scale(1); 670 | -moz-transform: scale(1); 671 | -ms-transform: scale(1); 672 | transform: scale(1); 673 | } 674 | } 675 | 676 | .tile-merged .tile-inner { 677 | z-index: 20; 678 | -webkit-animation: pop 200ms ease 100ms; 679 | -moz-animation: pop 200ms ease 100ms; 680 | animation: pop 200ms ease 100ms; 681 | -webkit-animation-fill-mode: backwards; 682 | -moz-animation-fill-mode: backwards; 683 | animation-fill-mode: backwards; 684 | } 685 | 686 | .above-game:after { 687 | content: ""; 688 | display: block; 689 | clear: both; 690 | } 691 | 692 | .game-intro, .guest-ip { 693 | float: left; 694 | line-height: 42px; 695 | margin-bottom: 0; 696 | } 697 | 698 | .connect-button { 699 | display: inline-block; 700 | background: #8f7a66; 701 | border-radius: 3px; 702 | padding: 0 20px; 703 | text-decoration: none; 704 | color: #f9f6f2; 705 | height: 40px; 706 | line-height: 42px; 707 | display: block; 708 | text-align: center; 709 | float: right; 710 | } 711 | 712 | .game-explanation { 713 | margin-top: 50px; 714 | } 715 | 716 | @media screen and (max-width: 520px) { 717 | html, body { 718 | font-size: 15px; 719 | } 720 | 721 | body { 722 | margin: 20px 0; 723 | padding: 0 20px; 724 | } 725 | 726 | h1.title { 727 | font-size: 27px; 728 | margin-top: 15px; 729 | } 730 | 731 | .container { 732 | width: 280px; 733 | margin: 0 auto; 734 | } 735 | 736 | .score-container, .best-container { 737 | margin-top: 0; 738 | padding: 15px 10px; 739 | min-width: 40px; 740 | } 741 | 742 | .heading { 743 | margin-bottom: 10px; 744 | } 745 | 746 | .game-intro { 747 | width: 55%; 748 | display: block; 749 | box-sizing: border-box; 750 | line-height: 1.65; 751 | } 752 | 753 | .connect-button { 754 | width: 42%; 755 | padding: 0; 756 | display: block; 757 | box-sizing: border-box; 758 | margin-top: 2px; 759 | } 760 | 761 | .game-container { 762 | margin-top: 17px; 763 | position: relative; 764 | padding: 10px; 765 | cursor: default; 766 | -webkit-touch-callout: none; 767 | -ms-touch-callout: none; 768 | -webkit-user-select: none; 769 | -moz-user-select: none; 770 | -ms-user-select: none; 771 | -ms-touch-action: none; 772 | touch-action: none; 773 | background: #bbada0; 774 | border-radius: 6px; 775 | width: 280px; 776 | height: 280px; 777 | -webkit-box-sizing: border-box; 778 | -moz-box-sizing: border-box; 779 | box-sizing: border-box; 780 | } 781 | 782 | .game-container .game-message { 783 | display: none; 784 | position: absolute; 785 | top: 0; 786 | right: 0; 787 | bottom: 0; 788 | left: 0; 789 | background: rgba(238, 228, 218, 0.5); 790 | z-index: 100; 791 | text-align: center; 792 | -webkit-animation: fade-in 800ms ease 1200ms; 793 | -moz-animation: fade-in 800ms ease 1200ms; 794 | animation: fade-in 800ms ease 1200ms; 795 | -webkit-animation-fill-mode: both; 796 | -moz-animation-fill-mode: both; 797 | animation-fill-mode: both; 798 | } 799 | 800 | .game-container .game-message p { 801 | font-size: 60px; 802 | font-weight: bold; 803 | height: 60px; 804 | line-height: 60px; 805 | margin-top: 222px; 806 | } 807 | 808 | .game-container .game-message .lower { 809 | display: block; 810 | margin-top: 59px; 811 | } 812 | 813 | .game-container .game-message a { 814 | display: inline-block; 815 | background: #8f7a66; 816 | border-radius: 3px; 817 | padding: 0 20px; 818 | text-decoration: none; 819 | color: #f9f6f2; 820 | height: 40px; 821 | line-height: 42px; 822 | margin-left: 9px; 823 | } 824 | 825 | .game-container .game-message a.keep-playing-button { 826 | display: none; 827 | } 828 | 829 | .game-container .game-message.game-won { 830 | background: rgba(237, 194, 46, 0.5); 831 | color: #f9f6f2; 832 | } 833 | 834 | .game-container .game-message.game-won a.keep-playing-button { 835 | display: inline-block; 836 | } 837 | 838 | .game-container .game-message.game-won, .game-container .game-message.game-over { 839 | display: block; 840 | } 841 | 842 | .grid-container { 843 | position: absolute; 844 | z-index: 1; 845 | } 846 | 847 | .grid-row { 848 | margin-bottom: 10px; 849 | } 850 | 851 | .grid-row:last-child { 852 | margin-bottom: 0; 853 | } 854 | 855 | .grid-row:after { 856 | content: ""; 857 | display: block; 858 | clear: both; 859 | } 860 | 861 | .grid-cell { 862 | width: 57.5px; 863 | height: 57.5px; 864 | margin-right: 10px; 865 | float: left; 866 | border-radius: 3px; 867 | background: rgba(238, 228, 218, 0.35); 868 | } 869 | 870 | .grid-cell:last-child { 871 | margin-right: 0; 872 | } 873 | 874 | .tile-container { 875 | position: absolute; 876 | z-index: 2; 877 | } 878 | 879 | .tile, .tile .tile-inner { 880 | width: 58px; 881 | height: 58px; 882 | line-height: 58px; 883 | } 884 | 885 | .tile.tile-position-1-1 { 886 | -webkit-transform: translate(0px, 0px); 887 | -moz-transform: translate(0px, 0px); 888 | -ms-transform: translate(0px, 0px); 889 | transform: translate(0px, 0px); 890 | } 891 | 892 | .tile.tile-position-1-2 { 893 | -webkit-transform: translate(0px, 67px); 894 | -moz-transform: translate(0px, 67px); 895 | -ms-transform: translate(0px, 67px); 896 | transform: translate(0px, 67px); 897 | } 898 | 899 | .tile.tile-position-1-3 { 900 | -webkit-transform: translate(0px, 135px); 901 | -moz-transform: translate(0px, 135px); 902 | -ms-transform: translate(0px, 135px); 903 | transform: translate(0px, 135px); 904 | } 905 | 906 | .tile.tile-position-1-4 { 907 | -webkit-transform: translate(0px, 202px); 908 | -moz-transform: translate(0px, 202px); 909 | -ms-transform: translate(0px, 202px); 910 | transform: translate(0px, 202px); 911 | } 912 | 913 | .tile.tile-position-2-1 { 914 | -webkit-transform: translate(67px, 0px); 915 | -moz-transform: translate(67px, 0px); 916 | -ms-transform: translate(67px, 0px); 917 | transform: translate(67px, 0px); 918 | } 919 | 920 | .tile.tile-position-2-2 { 921 | -webkit-transform: translate(67px, 67px); 922 | -moz-transform: translate(67px, 67px); 923 | -ms-transform: translate(67px, 67px); 924 | transform: translate(67px, 67px); 925 | } 926 | 927 | .tile.tile-position-2-3 { 928 | -webkit-transform: translate(67px, 135px); 929 | -moz-transform: translate(67px, 135px); 930 | -ms-transform: translate(67px, 135px); 931 | transform: translate(67px, 135px); 932 | } 933 | 934 | .tile.tile-position-2-4 { 935 | -webkit-transform: translate(67px, 202px); 936 | -moz-transform: translate(67px, 202px); 937 | -ms-transform: translate(67px, 202px); 938 | transform: translate(67px, 202px); 939 | } 940 | 941 | .tile.tile-position-3-1 { 942 | -webkit-transform: translate(135px, 0px); 943 | -moz-transform: translate(135px, 0px); 944 | -ms-transform: translate(135px, 0px); 945 | transform: translate(135px, 0px); 946 | } 947 | 948 | .tile.tile-position-3-2 { 949 | -webkit-transform: translate(135px, 67px); 950 | -moz-transform: translate(135px, 67px); 951 | -ms-transform: translate(135px, 67px); 952 | transform: translate(135px, 67px); 953 | } 954 | 955 | .tile.tile-position-3-3 { 956 | -webkit-transform: translate(135px, 135px); 957 | -moz-transform: translate(135px, 135px); 958 | -ms-transform: translate(135px, 135px); 959 | transform: translate(135px, 135px); 960 | } 961 | 962 | .tile.tile-position-3-4 { 963 | -webkit-transform: translate(135px, 202px); 964 | -moz-transform: translate(135px, 202px); 965 | -ms-transform: translate(135px, 202px); 966 | transform: translate(135px, 202px); 967 | } 968 | 969 | .tile.tile-position-4-1 { 970 | -webkit-transform: translate(202px, 0px); 971 | -moz-transform: translate(202px, 0px); 972 | -ms-transform: translate(202px, 0px); 973 | transform: translate(202px, 0px); 974 | } 975 | 976 | .tile.tile-position-4-2 { 977 | -webkit-transform: translate(202px, 67px); 978 | -moz-transform: translate(202px, 67px); 979 | -ms-transform: translate(202px, 67px); 980 | transform: translate(202px, 67px); 981 | } 982 | 983 | .tile.tile-position-4-3 { 984 | -webkit-transform: translate(202px, 135px); 985 | -moz-transform: translate(202px, 135px); 986 | -ms-transform: translate(202px, 135px); 987 | transform: translate(202px, 135px); 988 | } 989 | 990 | .tile.tile-position-4-4 { 991 | -webkit-transform: translate(202px, 202px); 992 | -moz-transform: translate(202px, 202px); 993 | -ms-transform: translate(202px, 202px); 994 | transform: translate(202px, 202px); 995 | } 996 | 997 | .tile .tile-inner { 998 | font-size: 35px; 999 | } 1000 | 1001 | .game-message p { 1002 | font-size: 30px !important; 1003 | height: 30px !important; 1004 | line-height: 30px !important; 1005 | margin-top: 90px !important; 1006 | } 1007 | 1008 | .game-message .lower { 1009 | margin-top: 30px !important; 1010 | } 1011 | } 1012 | 1013 | #home, #guest { 1014 | display: inline; 1015 | position: relative; 1016 | float: left; 1017 | margin-left: 10%; 1018 | margin-bottom: 5%; 1019 | } 1020 | 1021 | 1022 | 1023 | #footer { 1024 | float: left; 1025 | margin-left: 20%; 1026 | margin-top: 20px; 1027 | } --------------------------------------------------------------------------------