Use keys 1-4 in online game
[sumserum.git] / client / sumserum.js
1 //
2 // UI elements
3 //
4
5 var c=document.getElementById("canvas");
6 var ctx=c.getContext("2d");
7
8 // Some constants and enums
9 STONE_SIZE=25;
10 FIELD_WIDTH = STONE_SIZE * 2;
11 FIELD_HEIGHT = STONE_SIZE  * 2;
12 DIAGONAL = Math.pow(Math.pow(FIELD_WIDTH,2) + Math.pow(FIELD_HEIGHT,2), 0.5);
13 WOOD = "#A68064";
14
15 CENTER_X = c.width/2;
16 CENTER_Y = 0.75*FIELD_HEIGHT + (c.height - 0.75*FIELD_HEIGHT)/2;
17
18 var local = [];
19 var state;
20 var tentative_state;
21 var sockjs;
22
23 draw_message("Welcome to Sum Serum.");
24
25 // Masterreset
26 function no_game() {
27         state = undefined;
28         tentative_state = undefined;
29         ctx.clearRect(0, 0, c.width, c.height);
30
31         if (sockjs) sockjs.close();
32         sockjs = undefined;
33
34 }
35
36 // Start a game
37 function start_game(){
38         // Does not configure local
39         state = new State();
40         tentative_state = state;
41         state.restart_game();
42         draw_game();
43         c.focus();
44 }
45
46 // Buttons
47 document.getElementById("playlocal").addEventListener("click", function () {
48         if (state && state.phase != FINISHED) {
49                 if (!confirm("Do you really want to abort the current game?")) {
50                         return
51                 }
52         }
53         no_game();
54
55         local[PLAYER1] = true;
56         local[PLAYER2] = true;
57         start_game();
58 });
59
60 document.getElementById("playonline").addEventListener("click", function () {
61         if (state && state.phase != FINISHED) {
62                 if (!confirm("Do you really want to abort current game?")) {
63                         return
64                 }
65         }
66         no_game();
67
68         sockjs = new SockJS('/game');
69         draw_message("Connecting...");
70         sockjs.onmessage = sockjs_onmessage;
71         sockjs.onopen = sockjs_onopen;
72 });
73
74 window.addEventListener('beforeunload', function (e){
75         if (state && state.phase != FINISHED) {
76                 e.returnValue = "You have an unfinished game running."
77                 return e.returnValue;
78         }
79 });
80
81 // Handle server message
82 function sockjs_onopen(){
83         sockjs.send(JSON.stringify({meta: "hookmeup"}));
84         draw_message("Waiting for another player to join...");
85 }
86 function sockjs_onmessage(event) {
87         //console.log("data", event.data);
88         var input = JSON.parse(event.data);
89         var meta;
90         if (meta = input.meta) {
91                 // New game
92                 if (meta == "newgame") {
93                         local[input.you] = true;
94                         local[other(input.you)] = false;
95                         start_game();
96                 } else if (meta == "left") {
97                         no_game();
98                         draw_message("Your opponent has left the game.");
99                 } else if (meta == "error") {
100                         draw_message("Error: " + meta.error);
101                 } else {
102                         console.log(meta);
103                 }
104         } else {
105                 // Opponent interaction
106                 state.on_interaction(input)
107                 draw_game();
108         }
109 };
110
111 function interact(input) {
112         state.on_interaction(input);
113         // Send interaction if this is a remote game
114         if (sockjs && (local[PLAYER1] != local[PLAYER2])) sockjs.send(JSON.stringify(input));
115         draw_game();
116 }
117
118 // Keyboard bindings
119 function keys(side) {
120         // If side is the only local player, use 1-4 (the first set)
121         if (local[side] && ! local[other(side)]) return KEYS[PLAYER1]
122         // Otherwise, use both sets
123         return KEYS[side]
124 }
125
126
127 // Positioning functions
128 function at_field(coords, action) {
129         var n = coords[0];
130         var m = coords[1];
131         ctx.save();
132         ctx.translate(
133                 CENTER_X +                        n * FIELD_HEIGHT/2 - m * FIELD_HEIGHT / 2,
134                 CENTER_Y + 7 * FIELD_HEIGHT / 2 - n * FIELD_HEIGHT/2 - m * FIELD_HEIGHT / 2 );
135         action();
136         ctx.restore();
137 }
138
139 function at_player_box(side, action) {
140         ctx.save();
141         if (side == PLAYER1) {
142                 ctx.translate(FIELD_WIDTH/2, FIELD_HEIGHT/2);
143         } else {
144                 ctx.translate(500-FIELD_WIDTH/2-4*FIELD_WIDTH, FIELD_HEIGHT/2);
145         }
146         action();
147         ctx.restore();
148 }
149
150 function at_sel(n, action) {return function () {
151         ctx.save();
152         ctx.translate((n + 0.5) * FIELD_WIDTH, FIELD_HEIGHT/2);
153         action();
154         ctx.restore();
155 }}
156
157 // Drawing functions
158 function draw_field () {
159         //ctx.strokeStyle="#FF0000";
160         ctx.fillStyle=WOOD;
161         ctx.beginPath();
162         ctx.moveTo(-FIELD_WIDTH/2,0);
163         ctx.quadraticCurveTo(0, FIELD_HEIGHT/2, FIELD_WIDTH/2,0);
164         ctx.quadraticCurveTo(0, -FIELD_HEIGHT/2, -FIELD_WIDTH/2,0);
165         ctx.closePath();
166         ctx.fill();
167 };
168
169 function player_color(side) {
170         if (side == EMPTY) return "#C9A086";
171         if (side == PLAYER1) return "#000000";
172         if (side == PLAYER2) return "#FFFFFF";
173         if (side == SOON_PLAYER1) return "rgba(0,0,0,0.5)";
174         if (side == SOON_PLAYER2) return "rgba(255,255,255,0.5)";
175         console.log('Invalid side: ' + side);
176 }
177
178 function player_color_tentative(side) {
179         if (side == PLAYER1) return "rgba(0,0,0,0.5)";
180         if (side == PLAYER2) return "rgba(255,255,255,0.5)";
181         console.log('Invalid side: ' + side);
182 }
183
184 function gob_color(gob) {
185         if (gob == GOOD) return "green";
186         if (gob == BAD) return "red";
187         console.log('Invalid gob: ' + gob);
188 }
189
190 function draw_stone (side, halo, tentative) {return function() {
191         ctx.save();
192         ctx.fillStyle = player_color(EMPTY);
193         if (halo) {
194                 ctx.shadowColor = '#FFFFFF';
195                 ctx.shadowBlur = 8;
196                 ctx.shadowOffsetX = 0;
197                 ctx.shadowOffsetY = 0;
198         }
199         ctx.beginPath();
200         ctx.arc(0,0,STONE_SIZE/2, 0, 2*Math.PI);
201         ctx.fill();
202         ctx.restore();
203
204         if (tentative)
205                 ctx.fillStyle = player_color_tentative(side);
206         else
207                 ctx.fillStyle = player_color(side);
208         ctx.beginPath();
209         ctx.arc(0,0,STONE_SIZE/2, 0, 2*Math.PI);
210         ctx.fill();
211 }}
212
213 function at_each_segment(line, action) {
214         var m = line[0][0];
215         var n = line[0][1];
216         var m2 = line[1][0];
217         var n2 = line[1][1];
218         if (m == m2) {
219                 // Top left to bottom right
220                 if (n2 < n) { var tmp = n; n = n2 ; n2 = tmp }
221                 while (n < n2) {
222                         at_field([m, n], function () {
223                                 ctx.rotate(-45*Math.PI/180);
224                                 action();
225                         })
226                         n++;
227                 }
228         } else {
229                 // Bottom left to top right
230                 if (m2 < m) { var tmp = m; m = m2 ; m2 = tmp }
231                 while (m < m2) {
232                         at_field([m, n], function () {
233                                 ctx.rotate(45*Math.PI/180);
234                                 action();
235                         })
236                         m++;
237                 }
238         }
239 }
240
241 function draw_line(side, gob, line) {
242         at_each_segment(line, function () {
243                 ctx.fillStyle = player_color(side);
244                 ctx.beginPath();
245                 ctx.moveTo( STONE_SIZE/2 / Math.pow(2,0.5),
246                            -STONE_SIZE/2 / Math.pow(2,0.5));
247                 ctx.quadraticCurveTo(
248                             0,
249                            -DIAGONAL/4,
250                             STONE_SIZE/2 / Math.pow(2,0.5),
251                            -DIAGONAL/2
252                            +STONE_SIZE/2 / Math.pow(2,0.5));
253                 ctx.lineTo(
254                            -STONE_SIZE/2 / Math.pow(2,0.5),
255                            -DIAGONAL/2
256                            +STONE_SIZE/2 / Math.pow(2,0.5));
257                 ctx.quadraticCurveTo(
258                             0,
259                            -DIAGONAL/4,
260                            -STONE_SIZE/2 / Math.pow(2,0.5),
261                            -STONE_SIZE/2 / Math.pow(2,0.5));
262                 ctx.closePath;
263                 ctx.fill();
264         })
265         ctx.strokeStyle = gob_color(gob);
266         ctx.beginPath();
267         ctx.lineWidth = STONE_SIZE/6;
268         ctx.lineCap="round";
269         at_field(line[0], function() {ctx.moveTo(0,0)});
270         at_field(line[1], function() {ctx.lineTo(0,0)});
271         ctx.stroke();
272 }
273
274 function draw_player_box(side) {return function (){
275         ctx.fillStyle = WOOD;
276         ctx.beginPath();
277         ctx.lineJoin="round";
278         //ctx.moveTo(FIELD_HEIGHT/2, FIELD_HEIGHT);
279         ctx.arc(FIELD_HEIGHT/2, FIELD_HEIGHT/2,
280                 FIELD_HEIGHT/2, 0.5*Math.PI, 1.5*Math.PI);
281         ctx.lineTo(4*FIELD_WIDTH - FIELD_HEIGHT/2, 0);
282         ctx.arc(4*FIELD_WIDTH - FIELD_HEIGHT/2, FIELD_HEIGHT/2,
283                 FIELD_HEIGHT/2, 1.5*Math.PI, 0.5*Math.PI);
284         ctx.closePath();
285         ctx.fill();
286 }}
287
288 function draw_text(side, txt) {return function() {
289         ctx.fillStyle = player_color(side);
290         ctx.font = (0.5*FIELD_HEIGHT) + 'px sans-serif';
291         ctx.textAlign = 'center';
292         ctx.textBaseline = 'bottom';
293         ctx.fillText(txt, 2 * FIELD_WIDTH, 0.85 * FIELD_HEIGHT);
294 }}
295
296 function draw_message(msg) {
297         ctx.clearRect(0, 0, c.width, c.height);
298         ctx.fillStyle = player_color(PLAYER1);
299         ctx.font = (0.5*FIELD_HEIGHT) + 'px sans-serif';
300         ctx.textAlign = 'center';
301         ctx.textBaseline = 'bottom';
302         ctx.fillText(msg, CENTER_X,CENTER_Y);
303 }
304
305 function draw_key(side, i) {return function() {
306         draw_stone(EMPTY)();
307         var txt = keys(side)[i];
308         ctx.fillStyle = player_color(side);
309         ctx.font = (0.8*STONE_SIZE) + 'px sans-serif';
310         ctx.textAlign = 'center';
311         ctx.textBaseline = 'middle';
312         ctx.fillText(txt, 0, 0.1*STONE_SIZE);
313 }}
314
315 function draw_player_input(side) {return function (){
316         for (var i = 0; i < 4 ; i++) {
317                 at_sel(i, draw_key(side, i))();
318         }
319 }}
320
321 function draw_player_selection(side) {return function (){
322         for (var i = 0; i < 4 ; i++) {
323                 if (state.placed[side] <= i && i < tentative_state.placed[side]) {
324                         at_sel(i, draw_stone(side,false,true))();
325                 } else if (state.placed[side] <= i && i < state.chosen[side]) {
326                         at_sel(i, draw_stone(side))();
327                 } else {
328                         at_sel(i, draw_stone(EMPTY))();
329                 }
330         }
331 }}
332
333 function draw_game() {
334         ctx.clearRect(0, 0, c.width, c.height);
335         state.board.forEach(function (row, m) {
336                 row.forEach(function (player, n) {
337                         at_field([m, n], draw_field);
338                         at_field([m, n], draw_stone(
339                                 tentative_state.at([m,n]),
340                                 may_choose() && tentative_state.is_valid_field([m,n]),
341                                 state.at([m,n]) != tentative_state.at([m,n])
342                                 ));
343                 });
344         });
345         for (var side = PLAYER1; side <= PLAYER2; side++) {
346                 at_player_box(side, draw_player_box(side));
347                 if (state.phase == FINISHED) {
348                         var g = state.sumlength(state.good[side]);
349                         var b = state.sumlength(state.bad[side]);
350                         var total = g - b;
351                         at_player_box(side, draw_text(side, g + " \u2212 " + b + " = " + total));
352                 } else if (state.phase == CHOOSE) {
353                         // Never show remote stuff here
354                         if (local[side]){
355                                 if (state.chosen[side] == 0) {
356                                         at_player_box(side, draw_player_input(side));
357                                 }
358                                 // Only show the selected stuff in a remote game
359                                 if (state.chosen[side] > 0 && !local[other(side)]) {
360                                         at_player_box(side, draw_player_selection(side));
361                                 }
362                         }
363                 } else {
364                         at_player_box(side, draw_player_selection(side));
365                 }
366
367                 state.bad[side].forEach(function (line) {
368                         draw_line(side, BAD, line);
369                 })
370                 state.good[side].forEach(function (line) {
371                         draw_line(side, GOOD, line);
372                 })
373         }
374 }
375
376
377 // Global to local
378 function to_canvas(coords) {
379         var rect = canvas.getBoundingClientRect();
380         return [ coords[0] - rect.left, coords[1] - rect.top ];
381 }
382
383 // Coordinate to field
384 function to_field(coords) {
385         var x = coords[0];
386         var y = coords[1];
387         var m =    (x - CENTER_X)                   /FIELD_WIDTH
388                  - (y - CENTER_Y - 7*FIELD_HEIGHT/2)/FIELD_HEIGHT;
389         var n =  - (x - CENTER_X)                   /FIELD_WIDTH
390                  - (y - CENTER_Y - 7*FIELD_HEIGHT/2)/FIELD_HEIGHT;
391         if (Math.pow(m - Math.round(m), 2) + Math.pow(n - Math.round(n), 2) < 1/8) {
392                 m = Math.round(m);
393                 n = Math.round(n);
394                 if (m >= 0 && m < 7 && n >= 0 && n < 7 &&
395                    !(m == 0 && n == 0) && !(m == 6 && n == 6)) {
396                         return [m,n];
397                 }
398         }
399         return undefined;
400 }
401 // Coordinate to selector
402 function to_sel(coords) {
403         var x = coords[0];
404         var y = coords[1];
405
406         // Player 1
407         var i =    (x - FIELD_WIDTH/2  - FIELD_WIDTH/2)  / FIELD_WIDTH;
408         var j =  - (y - FIELD_HEIGHT/2 - FIELD_HEIGHT/2) / FIELD_HEIGHT;
409         if (Math.pow(i - Math.round(i), 2) + Math.pow(j - Math.round(j), 2) < 1/8) {
410                 i = Math.round(i);
411                 j = Math.round(j);
412                 if (i >= 0 && i < 4 && j == 0) {
413                         return { what: 'sel'
414                                , side: PLAYER1
415                                , n : i + 1};
416                 }
417         }
418
419         // Player 2
420         var i =    (x - (500 - FIELD_WIDTH/2  - FIELD_WIDTH/2 - 3*FIELD_WIDTH))/ FIELD_WIDTH;
421         var j =  - (y - FIELD_HEIGHT/2 - FIELD_HEIGHT/2) / FIELD_HEIGHT;
422         if (Math.pow(i - Math.round(i), 2) + Math.pow(j - Math.round(j), 2) < 1/8) {
423                 i = Math.round(i);
424                 j = Math.round(j);
425                 if (i >= 0 && i < 4 && j == 0) {
426                         return { what: 'sel'
427                                , side: PLAYER2
428                                , n : i + 1};
429                 }
430         }
431
432         return undefined;
433 }
434
435
436
437 // Event handling
438
439 function may_choose() {
440         return (state && state.phase == SELECT && local[state.to_place()[0]]);
441 }
442
443 var last_hover = undefined;
444 c.addEventListener('mousemove', function(evt) {
445         if (may_choose()) {
446                 var field = to_field(to_canvas([evt.clientX, evt.clientY]));
447                 if (field == last_hover
448                         || (field && last_hover && field[0] == last_hover[0] && field[1] == last_hover[1])) {
449                         ;
450                 } else {
451                         if (state) {
452                                 tentative_state = state;
453                                 if (field) {
454                                         tentative_state = state.clone();
455                                         tentative_state.on_interaction({what: 'field', field:field});
456                                 }
457                                 draw_game()
458                         }
459
460                         last_hover = field;
461                 }
462         }
463 });
464 c.addEventListener('mousedown', function(evt) {
465         if (may_choose()) {
466                 var field = to_field(to_canvas([evt.clientX, evt.clientY]));
467                 if (field) {
468                         tentative_state = state;
469                         var input = {what: 'field', field:field};
470                         interact(input);
471                         return
472                 }
473         }
474
475         var sel = to_sel(to_canvas([evt.clientX, evt.clientY]));
476         if (sel && local[sel.side]) {
477                 interact(sel);
478                 return
479         }
480
481         if (state && state.phase == FINISHED) {
482                 var input = {what: 'other'};
483                 interact(input);
484         }
485 });
486 c.addEventListener('keypress', function(evt) {
487         var key;
488         if (evt.charCode == 49) key = '1';
489         if (evt.charCode == 50) key = '2';
490         if (evt.charCode == 51) key = '3';
491         if (evt.charCode == 52) key = '4';
492         if (evt.charCode == 117) key = 'U';
493         if (evt.charCode == 105) key = 'I';
494         if (evt.charCode == 111) key = 'O';
495         if (evt.charCode == 112) key = 'P';
496         if (key) {
497                 var input;
498                 for (var side = PLAYER1; side <= PLAYER2; side++) {
499                         var i = keys(side).indexOf(key);
500                         if (i >= 0)
501                                 input = { what: 'sel', side: side, n: i + 1};
502                 }
503
504                 if (input && local[input.side]) {
505                         interact(input);
506                         evt.preventDefault();
507                 }
508         } else {
509                 //console.log(evt.charCode, key);
510         }
511 });