Skip to content

Commit 74a6618

Browse files
committed
2048
0 parents  commit 74a6618

File tree

11 files changed

+1521
-0
lines changed

11 files changed

+1521
-0
lines changed

2048.js

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
2+
const DIRECTION_LEFT = 1;
3+
const DIRECTION_UP = 2;
4+
const DIRECTION_RIGHT = 3;
5+
const DIRECTION_DOWN = 4;
6+
7+
class i2048 extends EventTarget {
8+
/**
9+
* @param {()=>number} rng
10+
*/
11+
constructor(rng) {
12+
super();
13+
this.rng = rng;
14+
this.score = 0;
15+
this._maxValue = 0;
16+
this.running = new Promise((x)=>x());
17+
}
18+
19+
_translate(dir) {
20+
switch(dir) {
21+
case DIRECTION_LEFT: return [1, 0, 0, 0, 0, 1];
22+
case DIRECTION_UP: return [0, 1, 0, 0, 1, 0];
23+
case DIRECTION_RIGHT: return [-1, 0, 3, 0, 0, 1];
24+
case DIRECTION_DOWN: return [0, -1, 0, 3, 1, 0];
25+
}
26+
}
27+
28+
/** @returns {Promise<boolean>} */
29+
move(dir) {
30+
const [dirX, dirY, startX, startY, runX, runY] = this._translate(dir);
31+
var result = false;
32+
var scoreGain = 0;
33+
for(var i = 0, aX = startX, aY = startY; i < 4; i++, aX += runX, aY += runY) {
34+
var destX = aX, destY = aY;
35+
var iX = aX + dirX, iY = aY + dirY;
36+
for(var j = 0; j < 3; j++, iX += dirX, iY += dirY) {
37+
const current = this._getValue(iX, iY);
38+
if(current === 0) continue;
39+
const dest = this._getValue(destX, destY);
40+
if(dest === 0) {
41+
this._tileMove(iX, iY, destX, destY);
42+
result = true;
43+
} else if(current === dest) {
44+
this._tileMerge(iX, iY, destX, destY, current * 2);
45+
scoreGain += current * 2;
46+
destX += dirX;
47+
destY += dirY;
48+
result = true;
49+
} else if(current.x == (destX + dirX) && current.y == (destY + dirY)) {
50+
destX += dirX;
51+
destY += dirY;
52+
continue;
53+
} else {
54+
// move tile and ceret
55+
destX += dirX;
56+
destY += dirY;
57+
if(iX != destX || iY != destY) {
58+
this._tileMove(iX, iY, destX, destY);
59+
result = true;
60+
}
61+
}
62+
}
63+
}
64+
if(scoreGain > 0) {
65+
this.score += scoreGain;
66+
this.dispatchEvent(new Event('scorechange'));
67+
}
68+
return new Promise(function(resolve) {
69+
resolve(result);
70+
});
71+
}
72+
73+
isValid(dir) {
74+
// This is just simplified move function
75+
const [dirX, dirY, startX, startY, runX, runY] = this._translate(dir);
76+
for(var i = 0, aX = startX, aY = startY; i < 4; i++, aX += runX, aY += runY) {
77+
var destX = aX, destY = aY;
78+
var iX = aX + dirX, iY = aY + dirY;
79+
for(var j = 0; j < 3; j++, iX += dirX, iY += dirY) {
80+
const current = this._getValue(iX, iY);
81+
if(current === 0) continue;
82+
const dest = this._getValue(destX, destY);
83+
if(dest === 0) {
84+
return true;
85+
} else if(current === dest) {
86+
return true;
87+
} else if(current.x == (destX + dirX) && current.y == (destY + dirY)) {
88+
destX += dirX;
89+
destY += dirY;
90+
continue;
91+
} else {
92+
destX += dirX;
93+
destY += dirY;
94+
if(iX != destX || iY != destY) {
95+
return true;
96+
}
97+
}
98+
}
99+
}
100+
return false;
101+
}
102+
103+
async start() {
104+
this.running = Promise.all([
105+
this.putRandomTile(),
106+
this.putRandomTile()
107+
]);
108+
await this.running;
109+
this.dispatchEvent(new Event('start'));
110+
}
111+
112+
putRandomTile() {
113+
var x, y;
114+
do {
115+
x = Math.floor(this.rng() * 4);
116+
y = Math.floor(this.rng() * 4);
117+
} while(this._getValue(x, y) !== 0);
118+
const value = this.rng() < 0.1 ? 4 : 2;
119+
return this._putTile(x, y, value);
120+
}
121+
122+
check() {
123+
for(var y = 0; y < 4; y++) {
124+
for(var x = 0; x < 4; x++) {
125+
if(this._getValue(x, y) == 0) {
126+
return true;
127+
}
128+
}
129+
}
130+
for(var y = 0; y < 4; y++) {
131+
for(var x = 0; x < 3; x++) {
132+
const a = this._getValue(x, y);
133+
const b = this._getValue(x + 1, y);
134+
if(a === b) return true;
135+
}
136+
for(var x = 0; x < 3; x++) {
137+
const a = this._getValue(y, x);
138+
const b = this._getValue(y, x + 1);
139+
if(a === b) return true;
140+
}
141+
}
142+
return false;
143+
}
144+
145+
_getValue(x, y) { return 0; }
146+
_tileMove(srcX, srcY, dstX, dstY) {}
147+
_tileMerge(srcX, srcY, dstX, dstY, newValue) {}
148+
/** @abstract @returns {Promise<void>} */
149+
_putTile(x, y, value) {}
150+
}

README.MD

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# 2048
2+
Clasic 2048 with replay feature.

bot.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
2+
/**
3+
* @typedef Bot2048
4+
* @property {(g: i2048)=>Promise<number>} getAction
5+
* @property {()=>void} cancel
6+
* @property {()=>void} attach
7+
* @property {()=>void} detach
8+
*/
9+
10+
class Bot2048Runner extends EventTarget {
11+
/**
12+
* @param {Bot2048} bot
13+
*/
14+
constructor(bot) {
15+
super();
16+
this.bot = bot;
17+
/** @type {()=>void} */
18+
this.event = null;
19+
this.playing = false;
20+
this.once = false;
21+
const legalAction = {};
22+
legalAction[DIRECTION_LEFT] = true;
23+
legalAction[DIRECTION_UP] = true;
24+
legalAction[DIRECTION_RIGHT] = true;
25+
legalAction[DIRECTION_DOWN] = true;
26+
this.legalAction = legalAction;
27+
Object.preventExtensions(legalAction);
28+
Object.preventExtensions(this);
29+
}
30+
31+
/** @param {i2048} board */
32+
doAction(board) {
33+
if(!this.playing) return;
34+
if(this.once) {
35+
this.playing = false;
36+
this.once = false;
37+
}
38+
if(!board.check()) {
39+
this.pause();
40+
return;
41+
}
42+
var _action ;
43+
this.bot.getAction(board).then(function(action) {
44+
_action = action;
45+
if(!(action in this.legalAction)) {
46+
return false;
47+
}
48+
return board.move(action);
49+
}.bind(this)).then(function(result) {
50+
if(result) return;
51+
alert('Invalid action: ' + result);
52+
this.pause();
53+
}.bind(this)).catch(function(cause) {
54+
if(this.playing) this.pause();
55+
}.bind(this));
56+
}
57+
58+
/** @param {i2048} board */
59+
attach(board) {
60+
this.event = function() {
61+
this.doAction(board);
62+
}.bind(this);
63+
board.addEventListener('tilepop', this.event);
64+
this.bot.attach();
65+
return this;
66+
}
67+
68+
/** @param {i2048} board */
69+
detach(board) {
70+
board.removeEventListener('tilepop', this.event);
71+
this.bot.cancel();
72+
this.bot.detach();
73+
}
74+
75+
async play() {
76+
this.playing = true;
77+
this.event();
78+
this.dispatchEvent(new Event('play'));
79+
}
80+
81+
step() {
82+
if(this.playing) this.pause();
83+
this.playing = true;
84+
this.once = true;
85+
this.event();
86+
}
87+
88+
pause() {
89+
this.playing = false;
90+
if(this.bot) {
91+
this.bot.cancel();
92+
}
93+
this.dispatchEvent(new Event('pause'));
94+
}
95+
}

game-controller.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
2+
/**
3+
* @param {EventTarget} pointerTarget
4+
* @param {EventTarget} keyTarget
5+
* @returns {Bot2048}
6+
*/
7+
function gameController(pointerTarget, keyTarget) {
8+
var resolve = null;
9+
var reject = function() {};
10+
const queue = [];
11+
12+
function move(dir) {
13+
if(resolve == null) {
14+
queue.push(dir);
15+
} else if(resolve(dir)) {
16+
resolve = null;
17+
}
18+
}
19+
20+
var _sx, _sy;
21+
function pointerStart(x, y) {
22+
_sx = x;
23+
_sy = y;
24+
}
25+
26+
function pointerEnd(x, y) {
27+
const dx = x - _sx;
28+
const dy = y - _sy;
29+
if(Math.hypot(dx, dy) < 30) return;
30+
if(Math.abs(dx) > Math.abs(dy)) {
31+
if(dx > 0) move(DIRECTION_RIGHT);
32+
else move(DIRECTION_LEFT);
33+
} else {
34+
if(dy > 0) move(DIRECTION_DOWN);
35+
else move(DIRECTION_UP);
36+
}
37+
}
38+
39+
function ontouchstart(ev) {
40+
pointerStart(ev.touches[0].screenX, ev.touches[0].screenY)
41+
}
42+
43+
function ontouchend(ev) {
44+
pointerEnd(ev.changedTouches[0].screenX, ev.changedTouches[0].screenY);
45+
}
46+
47+
function onpointerdown(ev) {
48+
pointerStart(ev.screenX, ev.screenY);
49+
}
50+
51+
function onpointerup(ev) {
52+
pointerEnd(ev.screenX, ev.screenY)
53+
}
54+
55+
function onkeydown(e) {
56+
switch(e.key) {
57+
case 'ArrowLeft':
58+
move(DIRECTION_LEFT);
59+
break;
60+
case 'ArrowUp':
61+
move(DIRECTION_UP);
62+
break;
63+
case 'ArrowDown':
64+
move(DIRECTION_DOWN);
65+
break;
66+
case 'ArrowRight':
67+
move(DIRECTION_RIGHT);
68+
break;
69+
}
70+
}
71+
72+
return {
73+
getAction: function(s) {
74+
return new Promise(function(_resolve, _reject) {
75+
reject = _reject;
76+
while(queue.length > 0) {
77+
var action = queue.shift()
78+
if(s.isValid(action)) {
79+
_resolve(action);
80+
return;
81+
}
82+
}
83+
resolve = function(dir) {
84+
if(s.isValid(dir)) {
85+
_resolve(dir);
86+
return true;
87+
}
88+
return false;
89+
};
90+
});
91+
},
92+
cancel: function() {
93+
reject("canceled");
94+
queue.splice(0, queue.length);
95+
},
96+
attach: function() {
97+
if(keyTarget) {
98+
keyTarget.addEventListener('keydown', onkeydown);
99+
}
100+
if(pointerTarget) {
101+
// pointerTarget.addEventListener('touchstart', ontouchstart);
102+
// pointerTarget.addEventListener('touchend', ontouchend);
103+
pointerTarget.addEventListener('pointerdown', onpointerdown);
104+
pointerTarget.addEventListener('pointerup', onpointerup);
105+
}
106+
},
107+
detach: function() {
108+
if(keyTarget) {
109+
keyTarget.removeEventListener('keydown', onkeydown);
110+
}
111+
if(pointerTarget) {
112+
// pointerTarget.removeEventListener('touchstart', ontouchstart);
113+
// pointerTarget.removeEventListener('touchend', ontouchend);
114+
pointerTarget.removeEventListener('pointerdown', onpointerdown);
115+
pointerTarget.removeEventListener('pointerup', onpointerup);
116+
}
117+
queue.splice(0, queue.length);
118+
}
119+
}
120+
}

0 commit comments

Comments
 (0)