├── 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 | 
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 |
31 |
32 |
33 |
Join the numbers and get to the 2048 tile!
34 |
Connect
35 |
36 |
37 |
38 |
45 |
46 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
88 |
89 |
92 |
93 |
94 |
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 | }
--------------------------------------------------------------------------------