Game Ready and Added PWA Support
After Width: | Height: | Size: 6.5 KiB |
After Width: | Height: | Size: 7.6 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 5.1 KiB |
After Width: | Height: | Size: 5.5 KiB |
After Width: | Height: | Size: 6.5 KiB |
After Width: | Height: | Size: 6.9 KiB |
After Width: | Height: | Size: 8.4 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 8.2 KiB |
After Width: | Height: | Size: 8.2 KiB |
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>
|
After Width: | Height: | Size: 1016 B |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 6.5 KiB |
After Width: | Height: | Size: 6.9 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 2.6 KiB |
|
@ -0,0 +1,549 @@
|
|||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Wait till the browser is ready to render the game (avoids glitches)
|
||||
window.requestAnimationFrame(function () {
|
||||
var manager = new GameManager(4, KeyboardInputManager, HTMLActuator);
|
||||
});
|
||||
});
|
||||
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker
|
||||
.register("./service-worker.js", {
|
||||
scope: ".",
|
||||
})
|
||||
.then(function (registration) {
|
||||
console.log("Registration successful, scope is:", registration.scope);
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.log("Service worker registration failed, error:", error);
|
||||
});
|
||||
}
|
||||
|
||||
function GameManager(size, InputManager, Actuator) {
|
||||
this.size = size; // Size of the grid
|
||||
this.inputManager = new InputManager();
|
||||
this.actuator = new Actuator();
|
||||
|
||||
this.startTiles = 2;
|
||||
|
||||
this.inputManager.on("move", this.move.bind(this));
|
||||
this.inputManager.on("restart", this.restart.bind(this));
|
||||
|
||||
this.setup();
|
||||
}
|
||||
|
||||
// Restart the game
|
||||
GameManager.prototype.restart = function () {
|
||||
this.actuator.restart();
|
||||
this.setup();
|
||||
};
|
||||
|
||||
// Set up the game
|
||||
GameManager.prototype.setup = function () {
|
||||
this.grid = new Grid(this.size);
|
||||
|
||||
this.score = 0;
|
||||
this.over = false;
|
||||
this.won = false;
|
||||
|
||||
// Add the initial tiles
|
||||
this.addStartTiles();
|
||||
|
||||
// Update the actuator
|
||||
this.actuate();
|
||||
};
|
||||
|
||||
// Set up the initial tiles to start the game with
|
||||
GameManager.prototype.addStartTiles = function () {
|
||||
for (var i = 0; i < this.startTiles; i++) {
|
||||
this.addRandomTile();
|
||||
}
|
||||
};
|
||||
|
||||
// Adds a tile in a random position
|
||||
GameManager.prototype.addRandomTile = function () {
|
||||
if (this.grid.cellsAvailable()) {
|
||||
var value = Math.random() < 0.9 ? 2 : 4;
|
||||
var tile = new Tile(this.grid.randomAvailableCell(), value);
|
||||
|
||||
this.grid.insertTile(tile);
|
||||
}
|
||||
};
|
||||
|
||||
// Sends the updated grid to the actuator
|
||||
GameManager.prototype.actuate = function () {
|
||||
this.actuator.actuate(this.grid, {
|
||||
score: this.score,
|
||||
over: this.over,
|
||||
won: this.won,
|
||||
});
|
||||
};
|
||||
|
||||
// Save all tile positions and remove merger info
|
||||
GameManager.prototype.prepareTiles = function () {
|
||||
this.grid.eachCell(function (x, y, tile) {
|
||||
if (tile) {
|
||||
tile.mergedFrom = null;
|
||||
tile.savePosition();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Move a tile and its representation
|
||||
GameManager.prototype.moveTile = function (tile, cell) {
|
||||
this.grid.cells[tile.x][tile.y] = null;
|
||||
this.grid.cells[cell.x][cell.y] = tile;
|
||||
tile.updatePosition(cell);
|
||||
};
|
||||
|
||||
// Move tiles on the grid in the specified direction
|
||||
GameManager.prototype.move = function (direction) {
|
||||
// 0: up, 1: right, 2:down, 3: left
|
||||
var self = this;
|
||||
|
||||
if (this.over || this.won) return; // Don't do anything if the game's over
|
||||
|
||||
var cell, tile;
|
||||
|
||||
var vector = this.getVector(direction);
|
||||
var traversals = this.buildTraversals(vector);
|
||||
var moved = false;
|
||||
|
||||
// Save the current tile positions and remove merger information
|
||||
this.prepareTiles();
|
||||
|
||||
// Traverse the grid in the right direction and move tiles
|
||||
traversals.x.forEach(function (x) {
|
||||
traversals.y.forEach(function (y) {
|
||||
cell = { x: x, y: y };
|
||||
tile = self.grid.cellContent(cell);
|
||||
|
||||
if (tile) {
|
||||
var positions = self.findFarthestPosition(cell, vector);
|
||||
var next = self.grid.cellContent(positions.next);
|
||||
|
||||
// Only one merger per row traversal?
|
||||
if (next && next.value === tile.value && !next.mergedFrom) {
|
||||
var merged = new Tile(positions.next, tile.value * 2);
|
||||
merged.mergedFrom = [tile, next];
|
||||
|
||||
self.grid.insertTile(merged);
|
||||
self.grid.removeTile(tile);
|
||||
|
||||
// Converge the two tiles' positions
|
||||
tile.updatePosition(positions.next);
|
||||
|
||||
// Update the score
|
||||
self.score += merged.value;
|
||||
|
||||
// The mighty 2048 tile
|
||||
if (merged.value === 2048) self.won = true;
|
||||
} else {
|
||||
self.moveTile(tile, positions.farthest);
|
||||
}
|
||||
|
||||
if (!self.positionsEqual(cell, tile)) {
|
||||
moved = true; // The tile moved from its original cell!
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (moved) {
|
||||
this.addRandomTile();
|
||||
|
||||
if (!this.movesAvailable()) {
|
||||
this.over = true; // Game over!
|
||||
}
|
||||
|
||||
this.actuate();
|
||||
}
|
||||
};
|
||||
|
||||
// Get the vector representing the chosen direction
|
||||
GameManager.prototype.getVector = function (direction) {
|
||||
// Vectors representing tile movement
|
||||
var map = {
|
||||
0: { x: 0, y: -1 }, // up
|
||||
1: { x: 1, y: 0 }, // right
|
||||
2: { x: 0, y: 1 }, // down
|
||||
3: { x: -1, y: 0 }, // left
|
||||
};
|
||||
|
||||
return map[direction];
|
||||
};
|
||||
|
||||
// Build a list of positions to traverse in the right order
|
||||
GameManager.prototype.buildTraversals = function (vector) {
|
||||
var traversals = { x: [], y: [] };
|
||||
|
||||
for (var pos = 0; pos < this.size; pos++) {
|
||||
traversals.x.push(pos);
|
||||
traversals.y.push(pos);
|
||||
}
|
||||
|
||||
// Always traverse from the farthest cell in the chosen direction
|
||||
if (vector.x === 1) traversals.x = traversals.x.reverse();
|
||||
if (vector.y === 1) traversals.y = traversals.y.reverse();
|
||||
|
||||
return traversals;
|
||||
};
|
||||
|
||||
GameManager.prototype.findFarthestPosition = function (cell, vector) {
|
||||
var previous;
|
||||
|
||||
// Progress towards the vector direction until an obstacle is found
|
||||
do {
|
||||
previous = cell;
|
||||
cell = { x: previous.x + vector.x, y: previous.y + vector.y };
|
||||
} while (this.grid.withinBounds(cell) && this.grid.cellAvailable(cell));
|
||||
|
||||
return {
|
||||
farthest: previous,
|
||||
next: cell, // Used to check if a merge is required
|
||||
};
|
||||
};
|
||||
|
||||
GameManager.prototype.movesAvailable = function () {
|
||||
return this.grid.cellsAvailable() || this.tileMatchesAvailable();
|
||||
};
|
||||
|
||||
// Check for available matches between tiles (more expensive check)
|
||||
GameManager.prototype.tileMatchesAvailable = function () {
|
||||
var self = this;
|
||||
|
||||
var tile;
|
||||
|
||||
for (var x = 0; x < this.size; x++) {
|
||||
for (var y = 0; y < this.size; y++) {
|
||||
tile = this.grid.cellContent({ x: x, y: y });
|
||||
|
||||
if (tile) {
|
||||
for (var direction = 0; direction < 4; direction++) {
|
||||
var vector = self.getVector(direction);
|
||||
var cell = { x: x + vector.x, y: y + vector.y };
|
||||
|
||||
var other = self.grid.cellContent(cell);
|
||||
if (other) {
|
||||
}
|
||||
|
||||
if (other && other.value === tile.value) {
|
||||
return true; // These two tiles can be merged
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
GameManager.prototype.positionsEqual = function (first, second) {
|
||||
return first.x === second.x && first.y === second.y;
|
||||
};
|
||||
|
||||
function Grid(size) {
|
||||
this.size = size;
|
||||
|
||||
this.cells = [];
|
||||
|
||||
this.build();
|
||||
}
|
||||
|
||||
// Build a grid of the specified size
|
||||
Grid.prototype.build = function () {
|
||||
for (var x = 0; x < this.size; x++) {
|
||||
var row = (this.cells[x] = []);
|
||||
|
||||
for (var y = 0; y < this.size; y++) {
|
||||
row.push(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Find the first available random position
|
||||
Grid.prototype.randomAvailableCell = function () {
|
||||
var cells = this.availableCells();
|
||||
|
||||
if (cells.length) {
|
||||
return cells[Math.floor(Math.random() * cells.length)];
|
||||
}
|
||||
};
|
||||
|
||||
Grid.prototype.availableCells = function () {
|
||||
var cells = [];
|
||||
|
||||
this.eachCell(function (x, y, tile) {
|
||||
if (!tile) {
|
||||
cells.push({ x: x, y: y });
|
||||
}
|
||||
});
|
||||
|
||||
return cells;
|
||||
};
|
||||
|
||||
// Call callback for every cell
|
||||
Grid.prototype.eachCell = function (callback) {
|
||||
for (var x = 0; x < this.size; x++) {
|
||||
for (var y = 0; y < this.size; y++) {
|
||||
callback(x, y, this.cells[x][y]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Check if there are any cells available
|
||||
Grid.prototype.cellsAvailable = function () {
|
||||
return !!this.availableCells().length;
|
||||
};
|
||||
|
||||
// Check if the specified cell is taken
|
||||
Grid.prototype.cellAvailable = function (cell) {
|
||||
return !this.cellOccupied(cell);
|
||||
};
|
||||
|
||||
Grid.prototype.cellOccupied = function (cell) {
|
||||
return !!this.cellContent(cell);
|
||||
};
|
||||
|
||||
Grid.prototype.cellContent = function (cell) {
|
||||
if (this.withinBounds(cell)) {
|
||||
return this.cells[cell.x][cell.y];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Inserts a tile at its position
|
||||
Grid.prototype.insertTile = function (tile) {
|
||||
this.cells[tile.x][tile.y] = tile;
|
||||
};
|
||||
|
||||
Grid.prototype.removeTile = function (tile) {
|
||||
this.cells[tile.x][tile.y] = null;
|
||||
};
|
||||
|
||||
Grid.prototype.withinBounds = function (position) {
|
||||
return (
|
||||
position.x >= 0 &&
|
||||
position.x < this.size &&
|
||||
position.y >= 0 &&
|
||||
position.y < this.size
|
||||
);
|
||||
};
|
||||
|
||||
function HTMLActuator() {
|
||||
this.tileContainer = document.getElementsByClassName("tile-container")[0];
|
||||
this.scoreContainer = document.getElementsByClassName("score-container")[0];
|
||||
this.messageContainer = document.getElementsByClassName("game-message")[0];
|
||||
|
||||
this.score = 0;
|
||||
}
|
||||
|
||||
HTMLActuator.prototype.actuate = function (grid, metadata) {
|
||||
var self = this;
|
||||
|
||||
window.requestAnimationFrame(function () {
|
||||
self.clearContainer(self.tileContainer);
|
||||
|
||||
grid.cells.forEach(function (column) {
|
||||
column.forEach(function (cell) {
|
||||
if (cell) {
|
||||
self.addTile(cell);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
self.updateScore(metadata.score);
|
||||
|
||||
if (metadata.over) self.message(false); // You lose
|
||||
if (metadata.won) self.message(true); // You win!
|
||||
});
|
||||
};
|
||||
|
||||
HTMLActuator.prototype.restart = function () {
|
||||
this.clearMessage();
|
||||
};
|
||||
|
||||
HTMLActuator.prototype.clearContainer = function (container) {
|
||||
while (container.firstChild) {
|
||||
container.removeChild(container.firstChild);
|
||||
}
|
||||
};
|
||||
|
||||
HTMLActuator.prototype.addTile = function (tile) {
|
||||
var self = this;
|
||||
|
||||
var element = document.createElement("div");
|
||||
var position = tile.previousPosition || { x: tile.x, y: tile.y };
|
||||
positionClass = this.positionClass(position);
|
||||
|
||||
// We can't use classlist because it somehow glitches when replacing classes
|
||||
var classes = ["tile", "tile-" + tile.value, positionClass];
|
||||
this.applyClasses(element, classes);
|
||||
|
||||
element.textContent = tile.value;
|
||||
|
||||
if (tile.previousPosition) {
|
||||
// Make sure that the tile gets rendered in the previous position first
|
||||
window.requestAnimationFrame(function () {
|
||||
classes[2] = self.positionClass({ x: tile.x, y: tile.y });
|
||||
self.applyClasses(element, classes); // Update the position
|
||||
});
|
||||
} else if (tile.mergedFrom) {
|
||||
classes.push("tile-merged");
|
||||
this.applyClasses(element, classes);
|
||||
|
||||
// Render the tiles that merged
|
||||
tile.mergedFrom.forEach(function (merged) {
|
||||
self.addTile(merged);
|
||||
});
|
||||
} else {
|
||||
classes.push("tile-new");
|
||||
this.applyClasses(element, classes);
|
||||
}
|
||||
|
||||
// Put the tile on the board
|
||||
this.tileContainer.appendChild(element);
|
||||
};
|
||||
|
||||
HTMLActuator.prototype.applyClasses = function (element, classes) {
|
||||
element.setAttribute("class", classes.join(" "));
|
||||
};
|
||||
|
||||
HTMLActuator.prototype.normalizePosition = function (position) {
|
||||
return { x: position.x + 1, y: position.y + 1 };
|
||||
};
|
||||
|
||||
HTMLActuator.prototype.positionClass = function (position) {
|
||||
position = this.normalizePosition(position);
|
||||
return "tile-position-" + position.x + "-" + position.y;
|
||||
};
|
||||
|
||||
HTMLActuator.prototype.updateScore = function (score) {
|
||||
this.clearContainer(this.scoreContainer);
|
||||
|
||||
var difference = score - this.score;
|
||||
this.score = score;
|
||||
|
||||
this.scoreContainer.textContent = this.score;
|
||||
|
||||
if (difference > 0) {
|
||||
var addition = document.createElement("div");
|
||||
addition.classList.add("score-addition");
|
||||
addition.textContent = "+" + difference;
|
||||
|
||||
this.scoreContainer.appendChild(addition);
|
||||
}
|
||||
};
|
||||
|
||||
HTMLActuator.prototype.message = function (won) {
|
||||
var type = won ? "game-won" : "game-over";
|
||||
var message = won ? "You win!" : "Game over!";
|
||||
|
||||
// if (ga) ga("send", "event", "game", "end", type, this.score);
|
||||
|
||||
this.messageContainer.classList.add(type);
|
||||
this.messageContainer.getElementsByTagName("p")[0].textContent = message;
|
||||
};
|
||||
|
||||
HTMLActuator.prototype.clearMessage = function () {
|
||||
this.messageContainer.classList.remove("game-won", "game-over");
|
||||
};
|
||||
|
||||
function KeyboardInputManager() {
|
||||
this.events = {};
|
||||
|
||||
this.listen();
|
||||
}
|
||||
|
||||
KeyboardInputManager.prototype.on = function (event, callback) {
|
||||
if (!this.events[event]) {
|
||||
this.events[event] = [];
|
||||
}
|
||||
this.events[event].push(callback);
|
||||
};
|
||||
|
||||
KeyboardInputManager.prototype.emit = function (event, data) {
|
||||
var callbacks = this.events[event];
|
||||
if (callbacks) {
|
||||
callbacks.forEach(function (callback) {
|
||||
callback(data);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
KeyboardInputManager.prototype.listen = function () {
|
||||
var self = this;
|
||||
|
||||
var map = {
|
||||
38: 0, // Up
|
||||
39: 1, // Right
|
||||
40: 2, // Down
|
||||
37: 3, // Left
|
||||
75: 0, // vim keybindings
|
||||
76: 1,
|
||||
74: 2,
|
||||
72: 3,
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", function (event) {
|
||||
var modifiers =
|
||||
event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
|
||||
var mapped = map[event.which];
|
||||
|
||||
if (!modifiers) {
|
||||
if (mapped !== undefined) {
|
||||
event.preventDefault();
|
||||
self.emit("move", mapped);
|
||||
}
|
||||
|
||||
if (event.which === 32) self.restart.bind(self)(event);
|
||||
}
|
||||
});
|
||||
|
||||
var retry = document.getElementsByClassName("retry-button")[0];
|
||||
retry.addEventListener("click", this.restart.bind(this));
|
||||
|
||||
// Listen to swipe events
|
||||
var gestures = [
|
||||
Hammer.DIRECTION_UP,
|
||||
Hammer.DIRECTION_RIGHT,
|
||||
Hammer.DIRECTION_DOWN,
|
||||
Hammer.DIRECTION_LEFT,
|
||||
];
|
||||
|
||||
var gameContainer = document.getElementsByClassName("game-container")[0];
|
||||
var handler = Hammer(gameContainer, {
|
||||
drag_block_horizontal: true,
|
||||
drag_block_vertical: true,
|
||||
});
|
||||
|
||||
handler.on("swipe", function (event) {
|
||||
event.gesture.preventDefault();
|
||||
mapped = gestures.indexOf(event.gesture.direction);
|
||||
|
||||
if (mapped !== -1) self.emit("move", mapped);
|
||||
});
|
||||
};
|
||||
|
||||
KeyboardInputManager.prototype.restart = function (event) {
|
||||
event.preventDefault();
|
||||
this.emit("restart");
|
||||
};
|
||||
|
||||
function Tile(position, value) {
|
||||
this.x = position.x;
|
||||
this.y = position.y;
|
||||
this.value = value || 2;
|
||||
|
||||
this.previousPosition = null;
|
||||
this.mergedFrom = null; // Tracks tiles that merged together
|
||||
}
|
||||
|
||||
Tile.prototype.savePosition = function () {
|
||||
this.previousPosition = { x: this.x, y: this.y };
|
||||
};
|
||||
|
||||
Tile.prototype.updatePosition = function (position) {
|
||||
this.x = position.x;
|
||||
this.y = position.y;
|
||||
};
|
|
@ -0,0 +1,867 @@
|
|||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #1d1f20;
|
||||
color: #776e65;
|
||||
font-family: "Clear Sans", "Helvetica Neue", Arial, sans-serif;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.heading:after {
|
||||
content: "";
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
h1.title {
|
||||
font-size: 80px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
display: block;
|
||||
float: left;
|
||||
}
|
||||
|
||||
@-webkit-keyframes move-up {
|
||||
0% {
|
||||
top: 25px;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: -50px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@-moz-keyframes move-up {
|
||||
0% {
|
||||
top: 25px;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: -50px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes move-up {
|
||||
0% {
|
||||
top: 25px;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: -50px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.score-container {
|
||||
position: relative;
|
||||
float: right;
|
||||
background: #303030;
|
||||
padding: 15px 25px;
|
||||
font-size: 25px;
|
||||
height: 25px;
|
||||
line-height: 47px;
|
||||
font-weight: bold;
|
||||
border-radius: 3px;
|
||||
color: white;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.score-container:after {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 10px;
|
||||
left: 0;
|
||||
content: "Score";
|
||||
text-transform: uppercase;
|
||||
font-size: 13px;
|
||||
line-height: 13px;
|
||||
text-align: center;
|
||||
color: #eee4da;
|
||||
}
|
||||
|
||||
.score-container .score-addition {
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
color: red;
|
||||
font-size: 25px;
|
||||
line-height: 25px;
|
||||
font-weight: bold;
|
||||
color: rgba(119, 110, 101, 0.9);
|
||||
z-index: 100;
|
||||
-webkit-animation: move-up 600ms ease-in;
|
||||
-moz-animation: move-up 600ms ease-in;
|
||||
-webkit-animation-fill-mode: both;
|
||||
-moz-animation-fill-mode: both;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #776e65;
|
||||
font-weight: bold;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
strong.important {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-bottom: 1px solid #d8d4d0;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@-webkit-keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@-moz-keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.game-container {
|
||||
margin-top: 10px;
|
||||
position: relative;
|
||||
padding: 15px;
|
||||
cursor: default;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
background: #303030;
|
||||
border-radius: 6px;
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.game-container .game-message {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: #1d1f20;
|
||||
opacity: 0.3;
|
||||
z-index: 100;
|
||||
text-align: center;
|
||||
-webkit-animation: fade-in 400ms ease 120ms;
|
||||
-moz-animation: fade-in 400ms ease 120ms;
|
||||
-webkit-animation-fill-mode: both;
|
||||
-moz-animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.game-container .game-message p {
|
||||
font-size: 60px;
|
||||
font-weight: bold;
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
margin-top: 222px;
|
||||
}
|
||||
|
||||
.game-container .game-message .lower {
|
||||
display: block;
|
||||
margin-top: 59px;
|
||||
}
|
||||
|
||||
.game-container .game-message a {
|
||||
display: inline-block;
|
||||
background: #8f7a66;
|
||||
border-radius: 3px;
|
||||
padding: 0 20px;
|
||||
text-decoration: none;
|
||||
color: #f9f6f2;
|
||||
height: 40px;
|
||||
line-height: 42px;
|
||||
margin-left: 9px;
|
||||
}
|
||||
|
||||
.game-container .game-message.game-won {
|
||||
background: #1d1f20;
|
||||
color: #f9f6f2;
|
||||
}
|
||||
|
||||
.game-container .game-message.game-won,
|
||||
.game-container .game-message.game-over {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.grid-row {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.grid-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.grid-row:after {
|
||||
content: "";
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.grid-cell {
|
||||
width: 106.25px;
|
||||
height: 106.25px;
|
||||
margin-right: 15px;
|
||||
float: left;
|
||||
border-radius: 3px;
|
||||
background: #1d1f20;
|
||||
}
|
||||
|
||||
.grid-cell:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.tile-container {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.tile {
|
||||
width: 106.25px;
|
||||
height: 106.25px;
|
||||
line-height: 116.25px;
|
||||
}
|
||||
|
||||
.tile.tile-position-1-1 {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.tile.tile-position-1-2 {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 121px;
|
||||
}
|
||||
|
||||
.tile.tile-position-1-3 {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 243px;
|
||||
}
|
||||
|
||||
.tile.tile-position-1-4 {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 364px;
|
||||
}
|
||||
|
||||
.tile.tile-position-2-1 {
|
||||
position: absolute;
|
||||
left: 121px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.tile.tile-position-2-2 {
|
||||
position: absolute;
|
||||
left: 121px;
|
||||
top: 121px;
|
||||
}
|
||||
|
||||
.tile.tile-position-2-3 {
|
||||
position: absolute;
|
||||
left: 121px;
|
||||
top: 243px;
|
||||
}
|
||||
|
||||
.tile.tile-position-2-4 {
|
||||
position: absolute;
|
||||
left: 121px;
|
||||
top: 364px;
|
||||
}
|
||||
|
||||
.tile.tile-position-3-1 {
|
||||
position: absolute;
|
||||
left: 243px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.tile.tile-position-3-2 {
|
||||
position: absolute;
|
||||
left: 243px;
|
||||
top: 121px;
|
||||
}
|
||||
|
||||
.tile.tile-position-3-3 {
|
||||
position: absolute;
|
||||
left: 243px;
|
||||
top: 243px;
|
||||
}
|
||||
|
||||
.tile.tile-position-3-4 {
|
||||
position: absolute;
|
||||
left: 243px;
|
||||
top: 364px;
|
||||
}
|
||||
|
||||
.tile.tile-position-4-1 {
|
||||
position: absolute;
|
||||
left: 364px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.tile.tile-position-4-2 {
|
||||
position: absolute;
|
||||
left: 364px;
|
||||
top: 121px;
|
||||
}
|
||||
|
||||
.tile.tile-position-4-3 {
|
||||
position: absolute;
|
||||
left: 364px;
|
||||
top: 243px;
|
||||
}
|
||||
|
||||
.tile.tile-position-4-4 {
|
||||
position: absolute;
|
||||
left: 364px;
|
||||
top: 364px;
|
||||
}
|
||||
|
||||
.tile {
|
||||
border-radius: 3px;
|
||||
background: #1d1f20;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
z-index: 10;
|
||||
font-size: 55px;
|
||||
-webkit-transition: 100ms ease-in-out;
|
||||
-moz-transition: 100ms ease-in-out;
|
||||
-webkit-transition-property: top, left;
|
||||
-moz-transition-property: top, left;
|
||||
}
|
||||
|
||||
.tile.tile-2 {
|
||||
background: #1d1f20;
|
||||
border: 3px solid #afafaf;
|
||||
color: #afafaf;
|
||||
box-shadow: 0 0 30px 10px #1d1f20 inset 0 0 0 1px #1d1f20;
|
||||
}
|
||||
|
||||
.tile.tile-4 {
|
||||
background: #1d1f20;
|
||||
border: 3px solid #f06292;
|
||||
color: #f06292;
|
||||
box-shadow: 0 0 30px 10px #afafaf inset 0 0 0 1px #afafaf;
|
||||
}
|
||||
|
||||
.tile.tile-8 {
|
||||
background: #1d1f20;
|
||||
border: 3px solid #ba68c8;
|
||||
color: #ba68c8;
|
||||
}
|
||||
|
||||
.tile.tile-16 {
|
||||
background: #1d1f20;
|
||||
border: 3px solid #9575cd;
|
||||
color: #9575cd;
|
||||
}
|
||||
|
||||
.tile.tile-32 {
|
||||
background: #1d1f20;
|
||||
border: 3px solid #7986cb;
|
||||
color: #7986cb;
|
||||
}
|
||||
|
||||
.tile.tile-64 {
|
||||
background: #1d1f20;
|
||||
border: 3px solid #64b5f6;
|
||||
color: #64b5f6;
|
||||
}
|
||||
|
||||
.tile.tile-128 {
|
||||
background: #1d1f20;
|
||||
border: 3px solid #64b5f6;
|
||||
color: #64b5f6;
|
||||
box-shadow: 0 0 5px 0 #7986cb, inset 0 0 0 1px #7986cb;
|
||||
font-size: 45px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
.tile.tile-128 {
|
||||
font-size: 25px;
|
||||
}
|
||||
}
|
||||
.tile.tile-256 {
|
||||
background: #1d1f20;
|
||||
border: 3px solid #4dd0e1;
|
||||
color: #4dd0e1;
|
||||
box-shadow: 0 0 10px 0 #4fc3f7, inset 0 0 0 1px #4fc3f7;
|
||||
font-size: 45px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
.tile.tile-256 {
|
||||
font-size: 25px;
|
||||
}
|
||||
}
|
||||
.tile.tile-512 {
|
||||
background: #1d1f20;
|
||||
border: 3px solid #4db6ac;
|
||||
color: #4db6ac;
|
||||
box-shadow: 0 0 15px 0 #4dd0e1, inset 0 0 0 1px #4dd0e1;
|
||||
font-size: 45px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
.tile.tile-512 {
|
||||
font-size: 25px;
|
||||
}
|
||||
}
|
||||
.tile.tile-1024 {
|
||||
background: #1d1f20;
|
||||
border: 3px solid #81c784;
|
||||
color: #81c784;
|
||||
box-shadow: 0 0 20px 0 #4db6ac, inset 0 0 0 1px #4db6ac;
|
||||
font-size: 35px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
.tile.tile-1024 {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
.tile.tile-2048 {
|
||||
--borderWidth: 3px;
|
||||
color: #1d1f20;
|
||||
background: #1d1f20;
|
||||
border-radius: var(--borderWidth);
|
||||
box-shadow: 0 0 30px 10px #1d1f20 inset 0 0 0 1px #1d1f20;
|
||||
}
|
||||
|
||||
.tile.tile-2048:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: calc(-1 * var(--borderWidth));
|
||||
left: calc(-1 * var(--borderWidth));
|
||||
height: calc(100% + var(--borderWidth) * 2);
|
||||
width: calc(100% + var(--borderWidth) * 2);
|
||||
background: linear-gradient(
|
||||
60deg,
|
||||
#f79533,
|
||||
#f37055,
|
||||
#ef4e7b,
|
||||
#a166ab,
|
||||
#5073b8,
|
||||
#1098ad,
|
||||
#07b39b,
|
||||
#6fba82
|
||||
);
|
||||
border-radius: calc(2 * var(--borderWidth));
|
||||
z-index: -1;
|
||||
animation: animatedgradient 3s ease alternate infinite;
|
||||
background-size: 300% 300%;
|
||||
}
|
||||
|
||||
@keyframes animatedgradient {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 480px) {
|
||||
.tile.tile-2048 {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes appear {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: scale(0);
|
||||
-moz-transform: scale(0);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
-webkit-transform: scale(1);
|
||||
-moz-transform: scale(1);
|
||||
}
|
||||
}
|
||||
@-moz-keyframes appear {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: scale(0);
|
||||
-moz-transform: scale(0);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
-webkit-transform: scale(1);
|
||||
-moz-transform: scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes appear {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: scale(0);
|
||||
-moz-transform: scale(0);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
-webkit-transform: scale(1);
|
||||
-moz-transform: scale(1);
|
||||
}
|
||||
}
|
||||
.tile-new {
|
||||
-webkit-animation: appear 200ms ease 100ms;
|
||||
-moz-animation: appear 200ms ease 100ms;
|
||||
-webkit-animation-fill-mode: both;
|
||||
-moz-animation-fill-mode: both;
|
||||
}
|
||||
|
||||
@-webkit-keyframes pop {
|
||||
0% {
|
||||
-webkit-transform: scale(0);
|
||||
-moz-transform: scale(0);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: scale(1.2);
|
||||
-moz-transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: scale(1);
|
||||
-moz-transform: scale(1);
|
||||
}
|
||||
}
|
||||
@-moz-keyframes pop {
|
||||
0% {
|
||||
-webkit-transform: scale(0);
|
||||
-moz-transform: scale(0);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: scale(1.2);
|
||||
-moz-transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: scale(1);
|
||||
-moz-transform: scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes pop {
|
||||
0% {
|
||||
-webkit-transform: scale(0);
|
||||
-moz-transform: scale(0);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: scale(1.2);
|
||||
-moz-transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: scale(1);
|
||||
-moz-transform: scale(1);
|
||||
}
|
||||
}
|
||||
.tile-merged {
|
||||
z-index: 20;
|
||||
-webkit-animation: pop 200ms ease 100ms;
|
||||
-moz-animation: pop 200ms ease 100ms;
|
||||
-webkit-animation-fill-mode: both;
|
||||
-moz-animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.game-intro {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.game-explanation {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
html,
|
||||
body {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 20px 0;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
h1.title {
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.score-container {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.game-container {
|
||||
margin-top: 40px;
|
||||
position: relative;
|
||||
padding: 10px;
|
||||
cursor: default;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
background: #303030;
|
||||
border-radius: 6px;
|
||||
width: 280px;
|
||||
height: 280px;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.game-container .game-message {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: rgba(238, 228, 218, 0.5);
|
||||
z-index: 100;
|
||||
text-align: center;
|
||||
-webkit-animation: fade-in 800ms ease 1200ms;
|
||||
-moz-animation: fade-in 800ms ease 1200ms;
|
||||
-webkit-animation-fill-mode: both;
|
||||
-moz-animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.game-container .game-message p {
|
||||
font-size: 60px;
|
||||
font-weight: bold;
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
margin-top: 222px;
|
||||
}
|
||||
|
||||
.game-container .game-message .lower {
|
||||
display: block;
|
||||
margin-top: 59px;
|
||||
}
|
||||
|
||||
.game-container .game-message a {
|
||||
display: inline-block;
|
||||
background: #8f7a66;
|
||||
border-radius: 3px;
|
||||
padding: 0 20px;
|
||||
text-decoration: none;
|
||||
color: #f9f6f2;
|
||||
height: 40px;
|
||||
line-height: 42px;
|
||||
margin-left: 9px;
|
||||
}
|
||||
|
||||
.game-container .game-message.game-won {
|
||||
background: rgba(237, 194, 46, 0.5);
|
||||
color: #f9f6f2;
|
||||
}
|
||||
|
||||
.game-container .game-message.game-won,
|
||||
.game-container .game-message.game-over {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.grid-row {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.grid-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.grid-row:after {
|
||||
content: "";
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.grid-cell {
|
||||
width: 57.5px;
|
||||
height: 57.5px;
|
||||
margin-right: 10px;
|
||||
float: left;
|
||||
border-radius: 3px;
|
||||
background: rgba(238, 228, 218, 0.35);
|
||||
}
|
||||
|
||||
.grid-cell:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.tile-container {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.tile {
|
||||
width: 57.5px;
|
||||
height: 57.5px;
|
||||
line-height: 67.5px;
|
||||
}
|
||||
|
||||
.tile.tile-position-1-1 {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.tile.tile-position-1-2 {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 68px;
|
||||
}
|
||||
|
||||
.tile.tile-position-1-3 {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 135px;
|
||||
}
|
||||
|
||||
.tile.tile-position-1-4 {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 203px;
|
||||
}
|
||||
|
||||
.tile.tile-position-2-1 {
|
||||
position: absolute;
|
||||
left: 68px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.tile.tile-position-2-2 {
|
||||
position: absolute;
|
||||
left: 68px;
|
||||
top: 68px;
|
||||
}
|
||||
|
||||
.tile.tile-position-2-3 {
|
||||
position: absolute;
|
||||
left: 68px;
|
||||
top: 135px;
|
||||
}
|
||||
|
||||
.tile.tile-position-2-4 {
|
||||
position: absolute;
|
||||
left: 68px;
|
||||
top: 203px;
|
||||
}
|
||||
|
||||
.tile.tile-position-3-1 {
|
||||
position: absolute;
|
||||
left: 135px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.tile.tile-position-3-2 {
|
||||
position: absolute;
|
||||
left: 135px;
|
||||
top: 68px;
|
||||
}
|
||||
|
||||
.tile.tile-position-3-3 {
|
||||
position: absolute;
|
||||
left: 135px;
|
||||
top: 135px;
|
||||
}
|
||||
|
||||
.tile.tile-position-3-4 {
|
||||
position: absolute;
|
||||
left: 135px;
|
||||
top: 203px;
|
||||
}
|
||||
|
||||
.tile.tile-position-4-1 {
|
||||
position: absolute;
|
||||
left: 203px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.tile.tile-position-4-2 {
|
||||
position: absolute;
|
||||
left: 203px;
|
||||
top: 68px;
|
||||
}
|
||||
|
||||
.tile.tile-position-4-3 {
|
||||
position: absolute;
|
||||
left: 203px;
|
||||
top: 135px;
|
||||
}
|
||||
|
||||
.tile.tile-position-4-4 {
|
||||
position: absolute;
|
||||
left: 203px;
|
||||
top: 203px;
|
||||
}
|
||||
|
||||
.game-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.tile {
|
||||
font-size: 35px;
|
||||
}
|
||||
|
||||
.game-message p {
|
||||
font-size: 30px !important;
|
||||
height: 30px !important;
|
||||
line-height: 30px !important;
|
||||
margin-top: 90px !important;
|
||||
}
|
||||
|
||||
.game-message .lower {
|
||||
margin-top: 30px !important;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
Source: https://github.com/coonsta/cache-polyfill
|
||||
Author: https://github.com/coonsta
|
||||
*/
|
||||
|
||||
if (!Cache.prototype.add) {
|
||||
Cache.prototype.add = function add(request) {
|
||||
return this.addAll([request]);
|
||||
};
|
||||
}
|
||||
|
||||
if (!Cache.prototype.addAll) {
|
||||
Cache.prototype.addAll = function addAll(requests) {
|
||||
var cache = this;
|
||||
|
||||
// Since DOMExceptions are not constructable:
|
||||
function NetworkError(message) {
|
||||
this.name = "NetworkError";
|
||||
this.code = 19;
|
||||
this.message = message;
|
||||
}
|
||||
NetworkError.prototype = Object.create(Error.prototype);
|
||||
|
||||
return Promise.resolve()
|
||||
.then(function () {
|
||||
if (arguments.length < 1) throw new TypeError();
|
||||
|
||||
// Simulate sequence<(Request or USVString)> binding:
|
||||
var sequence = [];
|
||||
|
||||
requests = requests.map(function (request) {
|
||||
if (request instanceof Request) {
|
||||
return request;
|
||||
} else {
|
||||
return String(request); // may throw TypeError
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all(
|
||||
requests.map(function (request) {
|
||||
if (typeof request === "string") {
|
||||
request = new Request(request);
|
||||
}
|
||||
|
||||
var scheme = new URL(request.url).protocol;
|
||||
|
||||
if (scheme !== "http:" && scheme !== "https:") {
|
||||
throw new NetworkError("Invalid scheme");
|
||||
}
|
||||
|
||||
return fetch(request.clone());
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(function (responses) {
|
||||
// TODO: check that requests don't overwrite one another
|
||||
// (don't think this is possible to polyfill due to opaque responses)
|
||||
return Promise.all(
|
||||
responses.map(function (response, i) {
|
||||
return cache.put(requests[i], response);
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(function () {
|
||||
return undefined;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
if (!CacheStorage.prototype.match) {
|
||||
// This is probably vulnerable to race conditions (removing caches etc)
|
||||
CacheStorage.prototype.match = function match(request, opts) {
|
||||
var caches = this;
|
||||
|
||||
return this.keys().then(function (cacheNames) {
|
||||
var match;
|
||||
|
||||
return cacheNames.reduce(function (chain, cacheName) {
|
||||
return chain.then(function () {
|
||||
return (
|
||||
match ||
|
||||
caches
|
||||
.open(cacheName)
|
||||
.then(function (cache) {
|
||||
return cache.match(request, opts);
|
||||
})
|
||||
.then(function (response) {
|
||||
match = response;
|
||||
return match;
|
||||
})
|
||||
);
|
||||
});
|
||||
}, Promise.resolve());
|
||||
});
|
||||
};
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>2048 | by Bravo 68 Web</title>
|
||||
<script src="./assets/scripts.js"></script>
|
||||
<link rel="stylesheet" href="./assets/style.css" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="57x57"
|
||||
href="./assets/favicons/apple-icon-57x57.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="60x60"
|
||||
href="./assets/favicons/apple-icon-60x60.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="72x72"
|
||||
href="./assets/favicons/apple-icon-72x72.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="76x76"
|
||||
href="./assets/favicons/apple-icon-76x76.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="114x114"
|
||||
href="./assets/favicons/apple-icon-114x114.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="120x120"
|
||||
href="./assets/favicons/apple-icon-120x120.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="144x144"
|
||||
href="./assets/favicons/apple-icon-144x144.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="152x152"
|
||||
href="./assets/favicons/apple-icon-152x152.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="./assets/favicons/apple-icon-180x180.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="192x192"
|
||||
href="./assets/favicons/android-icon-192x192.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="./assets/favicons/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="96x96"
|
||||
href="./assets/favicons/favicon-96x96.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="./assets/favicons/favicon-16x16.png"
|
||||
/>
|
||||
<link
|
||||
rel="manifest"
|
||||
href="./manifest.webmanifest"
|
||||
crossorigin="use-credentials"
|
||||
/>
|
||||
<meta name="msapplication-TileColor" content="#ffffff" />
|
||||
<meta
|
||||
name="msapplication-TileImage"
|
||||
content="./assets/favicons/ms-icon-144x144.png"
|
||||
/>
|
||||
<meta name="theme-color" content="#eb8e0c" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="heading">
|
||||
<h1 class="title">2048</h1>
|
||||
<div class="score-container">0</div>
|
||||
</div>
|
||||
|
||||
<div class="game-container">
|
||||
<div class="game-message">
|
||||
<p></p>
|
||||
<div class="lower">
|
||||
<a class="retry-button">RETRY</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-container">
|
||||
<div class="grid-row">
|
||||
<div class="grid-cell"></div>
|
||||
<div class="grid-cell"></div>
|
||||
<div class="grid-cell"></div>
|
||||
<div class="grid-cell"></div>
|
||||
</div>
|
||||
<div class="grid-row">
|
||||
<div class="grid-cell"></div>
|
||||
<div class="grid-cell"></div>
|
||||
<div class="grid-cell"></div>
|
||||
<div class="grid-cell"></div>
|
||||
</div>
|
||||
<div class="grid-row">
|
||||
<div class="grid-cell"></div>
|
||||
<div class="grid-cell"></div>
|
||||
<div class="grid-cell"></div>
|
||||
<div class="grid-cell"></div>
|
||||
</div>
|
||||
<div class="grid-row">
|
||||
<div class="grid-cell"></div>
|
||||
<div class="grid-cell"></div>
|
||||
<div class="grid-cell"></div>
|
||||
<div class="grid-cell"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tile-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/1.0.6/hammer.min.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"short_name": "2048",
|
||||
"name": "2048: Web-App Game",
|
||||
"description": "2048 WebApp game made by Bravo68web",
|
||||
"icons": [
|
||||
{
|
||||
"src": "./assets/favicons/android-icon-36x36.png",
|
||||
"sizes": "36x36",
|
||||
"type": "image/png",
|
||||
"density": "0.75"
|
||||
},
|
||||
{
|
||||
"src": "./assets/favicons/android-icon-48x48.png",
|
||||
"sizes": "48x48",
|
||||
"type": "image/png",
|
||||
"density": "1.0"
|
||||
},
|
||||
{
|
||||
"src": "./assets/favicons/android-icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png",
|
||||
"density": "1.5"
|
||||
},
|
||||
{
|
||||
"src": "./assets/favicons/android-icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png",
|
||||
"density": "2.0"
|
||||
},
|
||||
{
|
||||
"src": "./assets/favicons/android-icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png",
|
||||
"density": "3.0"
|
||||
},
|
||||
{
|
||||
"src": "./assets/favicons/android-icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"density": "4.0"
|
||||
}
|
||||
],
|
||||
"start_url": "/",
|
||||
"background_color": "#a80ceb",
|
||||
"display": "standalone"
|
||||
}
|
|
@ -0,0 +1,228 @@
|
|||
//Cache polyfil to support cacheAPI in all browsers
|
||||
importScripts("./cache-polyfill.js");
|
||||
|
||||
var cacheName = "cache-v4";
|
||||
|
||||
//Files to save in cache
|
||||
var files = [
|
||||
"./",
|
||||
"./index.html", //SW treats query string as new request
|
||||
"https://fonts.googleapis.com/css?family=Roboto:200,300,400,500,700", //caching 3rd party content
|
||||
"./assets/scripts.js",
|
||||
"./assets/style.js",
|
||||
"./manifest.webmanifest",
|
||||
"./assets/favicons/browserconfig.xml",
|
||||
"./assets/favicons/android-icon-36x36.png",
|
||||
"./assets/favicons/android-icon-48x48.png",
|
||||
"./assets/favicons/android-icon-72x72.png",
|
||||
"./assets/favicons/android-icon-96x96.png",
|
||||
"./assets/favicons/android-icon-144x144.png",
|
||||
"./assets/favicons/android-icon-192x192.png",
|
||||
"./assets/favicons/favicon-32x32.png",
|
||||
"./assets/favicons/favicon-16x16.png",
|
||||
"./assets/favicons/favicon-96x96.png",
|
||||
"./assets/favicons/favicon.ico",
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/hammer.js/1.0.6/hammer.min.js",
|
||||
];
|
||||
|
||||
//Adding `install` event listener
|
||||
self.addEventListener("install", (event) => {
|
||||
console.info("Event: Install");
|
||||
|
||||
event.waitUntil(
|
||||
caches.open(cacheName).then((cache) => {
|
||||
//[] of files to cache & if any of the file not present `addAll` will fail
|
||||
return cache
|
||||
.addAll(files)
|
||||
.then(() => {
|
||||
console.info("All files are cached");
|
||||
return self.skipWaiting(); //To forces the waiting service worker to become the active service worker
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to cache", error);
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
/*
|
||||
FETCH EVENT: triggered for every request made by index page, after install.
|
||||
*/
|
||||
|
||||
//Adding `fetch` event listener
|
||||
self.addEventListener("fetch", (event) => {
|
||||
console.info("Event: Fetch");
|
||||
|
||||
var request = event.request;
|
||||
var url = new URL(request.url);
|
||||
if (url.origin === location.origin) {
|
||||
// Static files cache
|
||||
event.respondWith(cacheFirst(request));
|
||||
} else {
|
||||
// Dynamic API cache
|
||||
event.respondWith(networkFirst(request));
|
||||
}
|
||||
|
||||
// // Checking for navigation preload response
|
||||
// if (event.preloadResponse) {
|
||||
// console.info('Using navigation preload');
|
||||
// return response;
|
||||
// }
|
||||
});
|
||||
|
||||
async function cacheFirst(request) {
|
||||
const cachedResponse = await caches.match(request);
|
||||
return cachedResponse || fetch(request);
|
||||
}
|
||||
|
||||
async function networkFirst(request) {
|
||||
const dynamicCache = await caches.open(cacheName);
|
||||
try {
|
||||
const networkResponse = await fetch(request);
|
||||
// Cache the dynamic API response
|
||||
dynamicCache.put(request, networkResponse.clone()).catch((err) => {
|
||||
console.warn(request.url + ": " + err.message);
|
||||
});
|
||||
return networkResponse;
|
||||
} catch (err) {
|
||||
const cachedResponse = await dynamicCache.match(request);
|
||||
return cachedResponse;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
ACTIVATE EVENT: triggered once after registering, also used to clean up caches.
|
||||
*/
|
||||
|
||||
//Adding `activate` event listener
|
||||
self.addEventListener("activate", (event) => {
|
||||
console.info("Event: Activate");
|
||||
|
||||
//Navigation preload is help us make parallel request while service worker is booting up.
|
||||
//Enable - chrome://flags/#enable-service-worker-navigation-preload
|
||||
//Support - Chrome 57 beta (behing the flag)
|
||||
//More info - https://developers.google.com/web/updates/2017/02/navigation-preload#the-problem
|
||||
|
||||
// Check if navigationPreload is supported or not
|
||||
// if (self.registration.navigationPreload) {
|
||||
// self.registration.navigationPreload.enable();
|
||||
// }
|
||||
// else if (!self.registration.navigationPreload) {
|
||||
// console.info('Your browser does not support navigation preload.');
|
||||
// }
|
||||
|
||||
//Remove old and unwanted caches
|
||||
event.waitUntil(
|
||||
caches
|
||||
.keys()
|
||||
.then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((cache) => {
|
||||
if (cache !== cacheName) {
|
||||
return caches.delete(cache); //Deleting the old cache (cache v1)
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(function () {
|
||||
console.info("Old caches are cleared!");
|
||||
// To tell the service worker to activate current one
|
||||
// instead of waiting for the old one to finish.
|
||||
return self.clients.claim();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
/*
|
||||
PUSH EVENT: triggered everytime, when a push notification is received.
|
||||
*/
|
||||
|
||||
//Adding `push` event listener
|
||||
self.addEventListener("push", (event) => {
|
||||
console.info("Event: Push");
|
||||
|
||||
var title = "Push notification demo";
|
||||
var body = {
|
||||
body: "click to return to application",
|
||||
tag: "demo",
|
||||
icon: "./images/icons/apple-touch-icon.png",
|
||||
badge: "./images/icons/apple-touch-icon.png",
|
||||
//Custom actions buttons
|
||||
actions: [
|
||||
{ action: "yes", title: "I ♥ this app!" },
|
||||
{ action: "no", title: "I don't like this app" },
|
||||
],
|
||||
};
|
||||
|
||||
event.waitUntil(self.registration.showNotification(title, body));
|
||||
});
|
||||
|
||||
/*
|
||||
BACKGROUND SYNC EVENT: triggers after `bg sync` registration and page has network connection.
|
||||
It will try and fetch github username, if its fulfills then sync is complete. If it fails,
|
||||
another sync is scheduled to retry (will will also waits for network connection)
|
||||
*/
|
||||
|
||||
self.addEventListener("sync", (event) => {
|
||||
console.info("Event: Sync");
|
||||
|
||||
//Check registered sync name or emulated sync from devTools
|
||||
if (event.tag === "github" || event.tag === "test-tag-from-devtools") {
|
||||
event.waitUntil(
|
||||
//To check all opened tabs and send postMessage to those tabs
|
||||
self.clients
|
||||
.matchAll()
|
||||
.then((all) => {
|
||||
return all.map((client) => {
|
||||
return client.postMessage("online"); //To make fetch request, check app.js - line no: 122
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
NOTIFICATION EVENT: triggered when user click the notification.
|
||||
*/
|
||||
|
||||
//Adding `notification` click event listener
|
||||
self.addEventListener("notificationclick", (event) => {
|
||||
var url = "https://demopwa.in/";
|
||||
|
||||
//Listen to custom action buttons in push notification
|
||||
if (event.action === "yes") {
|
||||
console.log("I ♥ this app!");
|
||||
} else if (event.action === "no") {
|
||||
console.warn("I don't like this app");
|
||||
}
|
||||
|
||||
event.notification.close(); //Close the notification
|
||||
|
||||
//To open the app after clicking notification
|
||||
event.waitUntil(
|
||||
clients
|
||||
.matchAll({
|
||||
type: "window",
|
||||
})
|
||||
.then((clients) => {
|
||||
for (var i = 0; i < clients.length; i++) {
|
||||
var client = clients[i];
|
||||
//If site is opened, focus to the site
|
||||
if (client.url === url && "focus" in client) {
|
||||
return client.focus();
|
||||
}
|
||||
}
|
||||
|
||||
//If site is cannot be opened, open in new window
|
||||
if (clients.openWindow) {
|
||||
return clients.openWindow("/");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
})
|
||||
);
|
||||
});
|