bc3bff456b6970fbb2ea08a84a60460fdde68650
[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
119 // Positioning functions
120 function at_field(coords, action) {
121         var n = coords[0];
122         var m = coords[1];
123         ctx.save();
124         ctx.translate(
125                 CENTER_X +                        n * FIELD_HEIGHT/2 - m * FIELD_HEIGHT / 2,
126                 CENTER_Y + 7 * FIELD_HEIGHT / 2 - n * FIELD_HEIGHT/2 - m * FIELD_HEIGHT / 2 );
127         action();
128         ctx.restore();
129 }
130
131 function at_player_box(side, action) {
132         ctx.save();
133         if (side == PLAYER1) {
134                 ctx.translate(FIELD_WIDTH/2, FIELD_HEIGHT/2);
135         } else {
136                 ctx.translate(500-FIELD_WIDTH/2-4*FIELD_WIDTH, FIELD_HEIGHT/2);
137         }
138         action();
139         ctx.restore();
140 }
141
142 function at_sel(n, action) {return function () {
143         ctx.save();
144         ctx.translate((n + 0.5) * FIELD_WIDTH, FIELD_HEIGHT/2);
145         action();
146         ctx.restore();
147 }}
148
149 // Drawing functions
150 function draw_field () {
151         //ctx.strokeStyle="#FF0000";
152         ctx.fillStyle=WOOD;
153         ctx.beginPath();
154         ctx.moveTo(-FIELD_WIDTH/2,0);
155         ctx.quadraticCurveTo(0, FIELD_HEIGHT/2, FIELD_WIDTH/2,0);
156         ctx.quadraticCurveTo(0, -FIELD_HEIGHT/2, -FIELD_WIDTH/2,0);
157         ctx.closePath();
158         ctx.fill();
159 };
160
161 function player_color(side) {
162         if (side == EMPTY) return "#C9A086";
163         if (side == PLAYER1) return "#000000";
164         if (side == PLAYER2) return "#FFFFFF";
165         if (side == SOON_PLAYER1) return "rgba(0,0,0,0.5)";
166         if (side == SOON_PLAYER2) return "rgba(255,255,255,0.5)";
167         console.log('Invalid side: ' + side);
168 }
169
170 function player_color_tentative(side) {
171         if (side == PLAYER1) return "rgba(0,0,0,0.5)";
172         if (side == PLAYER2) return "rgba(255,255,255,0.5)";
173         console.log('Invalid side: ' + side);
174 }
175
176 function gob_color(gob) {
177         if (gob == GOOD) return "green";
178         if (gob == BAD) return "red";
179         console.log('Invalid gob: ' + gob);
180 }
181
182 function draw_stone (side, halo, tentative) {return function() {
183         ctx.save();
184         ctx.fillStyle = player_color(EMPTY);
185         if (halo) {
186                 ctx.shadowColor = '#FFFFFF';
187                 ctx.shadowBlur = 8;
188                 ctx.shadowOffsetX = 0;
189                 ctx.shadowOffsetY = 0;
190         }
191         ctx.beginPath();
192         ctx.arc(0,0,STONE_SIZE/2, 0, 2*Math.PI);
193         ctx.fill();
194         ctx.restore();
195
196         if (tentative)
197                 ctx.fillStyle = player_color_tentative(side);
198         else
199                 ctx.fillStyle = player_color(side);
200         ctx.beginPath();
201         ctx.arc(0,0,STONE_SIZE/2, 0, 2*Math.PI);
202         ctx.fill();
203 }}
204
205 function at_each_segment(line, action) {
206         var m = line[0][0];
207         var n = line[0][1];
208         var m2 = line[1][0];
209         var n2 = line[1][1];
210         if (m == m2) {
211                 // Top left to bottom right
212                 if (n2 < n) { var tmp = n; n = n2 ; n2 = tmp }
213                 while (n < n2) {
214                         at_field([m, n], function () {
215                                 ctx.rotate(-45*Math.PI/180);
216                                 action();
217                         })
218                         n++;
219                 }
220         } else {
221                 // Bottom left to top right
222                 if (m2 < m) { var tmp = m; m = m2 ; m2 = tmp }
223                 while (m < m2) {
224                         at_field([m, n], function () {
225                                 ctx.rotate(45*Math.PI/180);
226                                 action();
227                         })
228                         m++;
229                 }
230         }
231 }
232
233 function draw_line(side, gob, line) {
234         at_each_segment(line, function () {
235                 ctx.fillStyle = player_color(side);
236                 ctx.beginPath();
237                 ctx.moveTo( STONE_SIZE/2 / Math.pow(2,0.5),
238                            -STONE_SIZE/2 / Math.pow(2,0.5));
239                 ctx.quadraticCurveTo(
240                             0,
241                            -DIAGONAL/4,
242                             STONE_SIZE/2 / Math.pow(2,0.5),
243                            -DIAGONAL/2
244                            +STONE_SIZE/2 / Math.pow(2,0.5));
245                 ctx.lineTo(
246                            -STONE_SIZE/2 / Math.pow(2,0.5),
247                            -DIAGONAL/2
248                            +STONE_SIZE/2 / Math.pow(2,0.5));
249                 ctx.quadraticCurveTo(
250                             0,
251                            -DIAGONAL/4,
252                            -STONE_SIZE/2 / Math.pow(2,0.5),
253                            -STONE_SIZE/2 / Math.pow(2,0.5));
254                 ctx.closePath;
255                 ctx.fill();
256         })
257         ctx.strokeStyle = gob_color(gob);
258         ctx.beginPath();
259         ctx.lineWidth = STONE_SIZE/6;
260         ctx.lineCap="round";
261         at_field(line[0], function() {ctx.moveTo(0,0)});
262         at_field(line[1], function() {ctx.lineTo(0,0)});
263         ctx.stroke();
264 }
265
266 function draw_player_box(side) {return function (){
267         ctx.fillStyle = WOOD;
268         ctx.beginPath();
269         ctx.lineJoin="round";
270         //ctx.moveTo(FIELD_HEIGHT/2, FIELD_HEIGHT);
271         ctx.arc(FIELD_HEIGHT/2, FIELD_HEIGHT/2,
272                 FIELD_HEIGHT/2, 0.5*Math.PI, 1.5*Math.PI);
273         ctx.lineTo(4*FIELD_WIDTH - FIELD_HEIGHT/2, 0);
274         ctx.arc(4*FIELD_WIDTH - FIELD_HEIGHT/2, FIELD_HEIGHT/2,
275                 FIELD_HEIGHT/2, 1.5*Math.PI, 0.5*Math.PI);
276         ctx.closePath();
277         ctx.fill();
278 }}
279
280 function draw_text(side, txt) {return function() {
281         ctx.fillStyle = player_color(side);
282         ctx.font = (0.5*FIELD_HEIGHT) + 'px sans-serif';
283         ctx.textAlign = 'center';
284         ctx.textBaseline = 'bottom';
285         ctx.fillText(txt, 2 * FIELD_WIDTH, 0.85 * FIELD_HEIGHT);
286 }}
287
288 function draw_message(msg) {
289         ctx.clearRect(0, 0, c.width, c.height);
290         ctx.fillStyle = player_color(PLAYER1);
291         ctx.font = (0.5*FIELD_HEIGHT) + 'px sans-serif';
292         ctx.textAlign = 'center';
293         ctx.textBaseline = 'bottom';
294         ctx.fillText(msg, CENTER_X,CENTER_Y);
295 }
296
297 function draw_key(side, i) {return function() {
298         draw_stone(EMPTY)();
299         var txt = KEYS[side][i];
300         ctx.fillStyle = player_color(side);
301         ctx.font = (0.8*STONE_SIZE) + 'px sans-serif';
302         ctx.textAlign = 'center';
303         ctx.textBaseline = 'middle';
304         ctx.fillText(txt, 0, 0.1*STONE_SIZE);
305 }}
306
307 function draw_player_input(side) {return function (){
308         for (var i = 0; i < 4 ; i++) {
309                 at_sel(i, draw_key(side, i))();
310         }
311 }}
312
313 function draw_player_selection(side) {return function (){
314         for (var i = 0; i < 4 ; i++) {
315                 if (state.placed[side] <= i && i < tentative_state.placed[side]) {
316                         at_sel(i, draw_stone(side,false,true))();
317                 } else if (state.placed[side] <= i && i < state.chosen[side]) {
318                         at_sel(i, draw_stone(side))();
319                 } else {
320                         at_sel(i, draw_stone(EMPTY))();
321                 }
322         }
323 }}
324
325 function draw_game() {
326         ctx.clearRect(0, 0, c.width, c.height);
327         state.board.forEach(function (row, m) {
328                 row.forEach(function (player, n) {
329                         at_field([m, n], draw_field);
330                         at_field([m, n], draw_stone(
331                                 tentative_state.at([m,n]),
332                                 may_choose() && tentative_state.is_valid_field([m,n]),
333                                 state.at([m,n]) != tentative_state.at([m,n])
334                                 ));
335                 });
336         });
337         for (var side = PLAYER1; side <= PLAYER2; side++) {
338                 at_player_box(side, draw_player_box(side));
339                 if (state.phase == FINISHED) {
340                         var g = state.sumlength(state.good[side]);
341                         var b = state.sumlength(state.bad[side]);
342                         var total = g - b;
343                         at_player_box(side, draw_text(side, g + " \u2212 " + b + " = " + total));
344                 } else if (state.phase == CHOOSE) {
345                         // Never show remote stuff here
346                         if (local[side]){
347                                 if (state.chosen[side] == 0) {
348                                         at_player_box(side, draw_player_input(side));
349                                 }
350                                 // Only show the selected stuff in a remote game
351                                 if (state.chosen[side] > 0 && !local[other(side)]) {
352                                         at_player_box(side, draw_player_selection(side));
353                                 }
354                         }
355                 } else {
356                         at_player_box(side, draw_player_selection(side));
357                 }
358
359                 state.bad[side].forEach(function (line) {
360                         draw_line(side, BAD, line);
361                 })
362                 state.good[side].forEach(function (line) {
363                         draw_line(side, GOOD, line);
364                 })
365         }
366 }
367
368
369 // Global to local
370 function to_canvas(coords) {
371         var rect = canvas.getBoundingClientRect();
372         return [ coords[0] - rect.left, coords[1] - rect.top ];
373 }
374
375 // Coordinate to field
376 function to_field(coords) {
377         var x = coords[0];
378         var y = coords[1];
379         var m =    (x - CENTER_X)                   /FIELD_WIDTH
380                  - (y - CENTER_Y - 7*FIELD_HEIGHT/2)/FIELD_HEIGHT;
381         var n =  - (x - CENTER_X)                   /FIELD_WIDTH
382                  - (y - CENTER_Y - 7*FIELD_HEIGHT/2)/FIELD_HEIGHT;
383         if (Math.pow(m - Math.round(m), 2) + Math.pow(n - Math.round(n), 2) < 1/8) {
384                 m = Math.round(m);
385                 n = Math.round(n);
386                 if (m >= 0 && m < 7 && n >= 0 && n < 7 &&
387                    !(m == 0 && n == 0) && !(m == 6 && n == 6)) {
388                         return [m,n];
389                 }
390         }
391         return undefined;
392 }
393 // Coordinate to selector
394 function to_sel(coords) {
395         var x = coords[0];
396         var y = coords[1];
397
398         // Player 1
399         var i =    (x - FIELD_WIDTH/2  - FIELD_WIDTH/2)  / FIELD_WIDTH;
400         var j =  - (y - FIELD_HEIGHT/2 - FIELD_HEIGHT/2) / FIELD_HEIGHT;
401         if (Math.pow(i - Math.round(i), 2) + Math.pow(j - Math.round(j), 2) < 1/8) {
402                 i = Math.round(i);
403                 j = Math.round(j);
404                 if (i >= 0 && i < 4 && j == 0) {
405                         return { what: 'sel'
406                                , side: PLAYER1
407                                , n : i + 1};
408                 }
409         }
410
411         // Player 2
412         var i =    (x - (500 - FIELD_WIDTH/2  - FIELD_WIDTH/2 - 3*FIELD_WIDTH))/ FIELD_WIDTH;
413         var j =  - (y - FIELD_HEIGHT/2 - FIELD_HEIGHT/2) / FIELD_HEIGHT;
414         if (Math.pow(i - Math.round(i), 2) + Math.pow(j - Math.round(j), 2) < 1/8) {
415                 i = Math.round(i);
416                 j = Math.round(j);
417                 if (i >= 0 && i < 4 && j == 0) {
418                         return { what: 'sel'
419                                , side: PLAYER2
420                                , n : i + 1};
421                 }
422         }
423
424         return undefined;
425 }
426
427
428
429 // Event handling
430
431 function may_choose() {
432         return (state && state.phase == SELECT && local[state.to_place()[0]]);
433 }
434
435 var last_hover = undefined;
436 c.addEventListener('mousemove', function(evt) {
437         if (may_choose()) {
438                 var field = to_field(to_canvas([evt.clientX, evt.clientY]));
439                 if (field == last_hover
440                         || (field && last_hover && field[0] == last_hover[0] && field[1] == last_hover[1])) {
441                         ;
442                 } else {
443                         if (state) {
444                                 tentative_state = state;
445                                 if (field) {
446                                         tentative_state = state.clone();
447                                         tentative_state.on_interaction({what: 'field', field:field});
448                                 }
449                                 draw_game()
450                         }
451
452                         last_hover = field;
453                 }
454         }
455 });
456 c.addEventListener('mousedown', function(evt) {
457         if (may_choose()) {
458                 var field = to_field(to_canvas([evt.clientX, evt.clientY]));
459                 if (field) {
460                         tentative_state = state;
461                         var input = {what: 'field', field:field};
462                         interact(input);
463                         return
464                 }
465         }
466
467         var sel = to_sel(to_canvas([evt.clientX, evt.clientY]));
468         if (sel && local[sel.side]) {
469                 interact(sel);
470                 return
471         }
472
473         if (state && state.phase == FINISHED) {
474                 var input = {what: 'other'};
475                 interact(input);
476         }
477 });
478 c.addEventListener('keypress', function(evt) {
479         var key;
480         if (evt.charCode == 49) key = '1';
481         if (evt.charCode == 50) key = '2';
482         if (evt.charCode == 51) key = '3';
483         if (evt.charCode == 52) key = '4';
484         if (evt.charCode == 117) key = 'U';
485         if (evt.charCode == 105) key = 'I';
486         if (evt.charCode == 111) key = 'O';
487         if (evt.charCode == 112) key = 'P';
488         if (key) {
489                 var input;
490                 for (var side = PLAYER1; side <= PLAYER2; side++) {
491                         var i = KEYS[side].indexOf(key);
492                         if (i >= 0)
493                                 input = { what: 'sel', side: side, n: i + 1};
494                 }
495
496                 if (input && local[input.side]) {
497                         interact(input);
498                         evt.preventDefault();
499                 }
500         } else {
501                 //console.log(evt.charCode, key);
502         }
503 });