Correctly detect a game as finished if all pieces are placed
[sumserum.git] / client / engine.js
1 //
2 // The engine (i.e. game state,  game mechanics, UI hooks)
3 //
4
5
6 // Some constants and enums
7 EMPTY = 0;
8 PLAYER1 = 1;
9 PLAYER2 = 2;
10 GOOD = 1;
11 BAD = 2;
12
13 var KEYS = [];
14 KEYS[PLAYER1] = ['1','2','3','4'];
15 KEYS[PLAYER2] = ['U','I','O','P'];
16
17 // Phases
18 var CHOOSE = 1;
19 var SELECT = 2;
20 var FINISHED = 3;
21
22
23 // Game state
24
25 function State(){
26 }
27
28 State.prototype.restart_game = function() {
29         this.current_side = PLAYER1;
30         this.phase = CHOOSE;
31         this.board = this.empty_board();
32         this.chosen = [];
33         this.chosen[PLAYER1] = 0;
34         this.chosen[PLAYER2] = 0;
35         this.placed=[];
36         this.placed[PLAYER1] = 0;
37         this.placed[PLAYER2] = 0;
38
39         this.good = [];
40         this.good[PLAYER1] = [];
41         this.good[PLAYER2] = [];
42         this.bad = [];
43         this.bad[PLAYER1] = [];
44         this.bad[PLAYER2] = [];
45 };
46
47 State.prototype.clone = function () {
48         var clone = new State();
49         var props = deepcopy(this);
50         for (p in props){
51                 clone[p] = props[p];
52         }
53         return clone;
54 };
55
56 // Game mechanics
57 State.prototype.to_phase = function (new_phase) {
58         if (new_phase == CHOOSE) {
59                 this.chosen[PLAYER1] = 0;
60                 this.chosen[PLAYER2] = 0;
61                 this.placed[PLAYER1] = 0;
62                 this.placed[PLAYER2] = 0;
63                 this.phase = CHOOSE;
64         }
65         if (new_phase == SELECT) {
66                 if (this.chosen[PLAYER1] < this.chosen[PLAYER2]) {
67                         this.current_side = PLAYER1
68                 } else if (this.chosen[PLAYER2] < this.chosen[PLAYER1]) {
69                         this.current_side = PLAYER2
70                 }
71                 this.phase = SELECT;
72         }
73         if (new_phase == FINISHED) {
74                 this.tally();
75                 this.phase = FINISHED;
76         }
77 };
78
79
80
81 // A list of stones left to place.
82 State.prototype.to_place = function () {
83         var ret = []
84         for (var i = 0; i < this.chosen[this.current_side] - this.placed[this.current_side]; i++)
85                 ret.push(this.current_side);
86         for (var i = 0; i < this.chosen[other(this.current_side)] - this.placed[other(this.current_side)]; i++)
87                 ret.push(other(this.current_side));
88         return ret;
89 };
90
91 // Reacting on user interaction
92 State.prototype.on_interaction = function(input){
93         if (input.what ==  "sel") {
94                 if (this.phase == CHOOSE) {
95                         this.chosen[input.side] = input.n;
96                         if (this.chosen[PLAYER1] > 0 && this.chosen[PLAYER2] > 0) {
97                                 this.to_phase(SELECT);
98                         }
99                 }
100
101         }
102         if (input.what == "field") {
103                 var field = input.field;
104                 if (this.phase == SELECT) {
105                         var row_rest = this.is_valid_field(field);
106                         if (row_rest) {
107                                 var field, player;
108                                 var todo = this.to_place();
109                                 while ((field = row_rest.shift()) && (side = todo.shift())){
110                                         this.board[field[0]][field[1]] = side;
111                                         this.placed[side]++;
112                                 }
113
114                                 if (this.is_game_finished()) {
115                                         this.to_phase(FINISHED)
116                                 } else  {
117                                         if (this.placed[PLAYER1] == this.chosen[PLAYER1] &&
118                                             this.placed[PLAYER2] == this.chosen[PLAYER2]) {
119                                                 this.to_phase(CHOOSE);
120                                         }
121                                 }
122                         }
123                 }
124         }
125         if (input.what == "other") {
126                 if (this.phase == FINISHED) {
127                         this.restart_game();
128                 }
129         }
130 };
131
132 // An empty board
133 State.prototype.empty_board = function () {
134         var board = [];
135         for (m = 0 ; m < 7; m++) {
136                 if (!board[m]) board[m] = [];
137                 for (n = 0 ; n < 7; n++) {
138                         if (m == 0 && n == 0 || m == 6 && n == 6) continue;
139                         // board[m][n] = 1 + Math.floor( 2 * Math.random());
140                         board[m][n] = EMPTY;
141                 }
142         }
143         return board;
144 };
145
146 // Tallying the result
147 State.prototype.tally = function () {
148         // Top left to bottom right
149         for (var m = 0; m < 7; m++) {
150                 var i = 0;
151                 if (m == 0) i = 1;
152                 for (var n = i+1; n <= 7; n++) {
153                         if (n == 7 || m == 6 && n == 6 || this.at([m,n]) != this.at([m,i])) {
154                                 if (n - i == 3) {
155                                         this.good[this.at([m,i])].push([[m,i], [m,n-1]]);
156                                 }
157                                 if (n - i > 3) {
158                                         this.bad[this.at([m,i])].push([[m,i], [m,n-1]]);
159                                 }
160                                 i = n;
161                         }
162                 }
163         }
164         // Bottom left to top right
165         for (var n = 0; n < 7; n++) {
166                 var i = 0;
167                 if (n == 0) i = 1;
168                 for (var m = i+1; m <= 7; m++) {
169                         if (m == 7 || n == 6 && m == 6 || this.at([m,n]) != this.at([i,n])) {
170                                 if (m - i == 3) {
171                                         this.good[this.at([i,n])].push([[i,n], [m-1,n]]);
172                                 }
173                                 if (m - i > 3) {
174                                         this.bad[this.at([i,n])].push([[i,n], [m-1,n]]);
175                                 }
176                                 i = m;
177                         }
178                 }
179         }
180 };
181
182 State.prototype.at = function(coord) {
183         return this.board[coord[0]][coord[1]];
184 }
185
186 State.prototype.is_empty = function(coord) {
187         var side = this.at(coord)
188         return !(side == PLAYER1 || side == PLAYER2);
189 }
190
191 State.prototype.sumlength = function(rows) {
192         var res = 0;
193         rows.forEach(function (r) {
194                 res += r[1][0] - r[0][0] + r[1][1] - r[0][1] + 1
195         });
196         return res;
197 }
198
199 // All rows, and their coordinates
200 State.prototype.rows = (function() {
201         var rows = [];
202         for (var i = 1; i< 12; i++) { // 5 rows, from bottom to top
203                 row = [];
204                 for (var j = 0; j < Math.min(i + 1, 13 - i) ; j++) {
205                         row[j] = [Math.max(0, i - 6) + j,
206                                  Math.min(i, 6)     - j];
207                 }
208                 rows.push(row);
209         }
210         return rows;
211 })();
212
213 State.prototype.valid_rows = function () {
214         var valid_rows = [];
215         for (var i = 0; i < this.rows.length; i++) {
216                 if (this.is_empty(this.rows[i][0]) &&
217                     this.is_empty(this.rows[i][this.rows[i].length - 1])) {
218                         // Row is empty?
219                         valid_rows.push(this.rows[i].slice());
220                         valid_rows.push(this.rows[i].slice().reverse());
221                 }
222                 else if (!this.is_empty(this.rows[i][0]) &&
223                     !this.is_empty(this.rows[i][this.rows[i].length - 1])) {
224                         // Row is full
225                 }
226                 // Partial row!
227                 else if (this.is_empty(this.rows[i][0])) {
228                         // Filled from the right
229                         for (var j = 0; this.is_empty(this.rows[i][j]); j++) {
230                         }
231                         return [ this.rows[i].slice(0,j).reverse() ];
232                 }
233                 else if (this.is_empty(this.rows[i][this.rows[i].length - 1])) {
234                         // Filled from the left
235                         for (var j = this.rows[i].length - 1; this.is_empty(this.rows[i][j]); j--) {
236                         }
237                         return [ this.rows[i].slice(j+1) ];
238                 }
239
240         }
241         return valid_rows;
242 }
243
244 State.prototype.is_game_finished = function() {
245         return this.valid_rows().length == 0;
246 }
247
248 // All the coordinates of the row of one field
249 State.prototype.row_of = function(field) {
250         var m = field[0];
251         var n = field[1];
252         var row = [];
253         return row;
254 }
255
256 // Check if something can be placed here
257 State.prototype.is_valid_field = function (field) {
258         if (this.phase == SELECT) {
259                 var vrows = this.valid_rows();
260                 for (var i = 0; i < vrows.length; i++) {
261                         if (vrows[i][0][0] == field[0] && vrows[i][0][1] == field[1]) {
262                                 return vrows[i].slice();
263                         }
264                 }
265         }
266         return undefined;
267 }
268
269
270 // Utilities
271 function deepcopy(x) {return JSON.parse(JSON.stringify(x))}
272 function and(f1,f2) {return function() { f1(); f2() } }
273
274 function other(side) {
275         if (side == PLAYER1) return PLAYER2;
276         if (side == PLAYER2) return PLAYER1;
277 }
278