Refactoring samegame
[profile/ivi/qtdeclarative.git] / examples / demos / samegame / content / samegame.js
1 /****************************************************************************
2 **
3 ** Copyright (C) 2012 Digia Plc and/or its subsidiary(-ies).
4 ** Contact: http://www.qt-project.org/legal
5 **
6 ** This file is part of the examples of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:BSD$
9 ** You may use this file under the terms of the BSD license as follows:
10 **
11 ** "Redistribution and use in source and binary forms, with or without
12 ** modification, are permitted provided that the following conditions are
13 ** met:
14 **   * Redistributions of source code must retain the above copyright
15 **     notice, this list of conditions and the following disclaimer.
16 **   * Redistributions in binary form must reproduce the above copyright
17 **     notice, this list of conditions and the following disclaimer in
18 **     the documentation and/or other materials provided with the
19 **     distribution.
20 **   * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names
21 **     of its contributors may be used to endorse or promote products derived
22 **     from this software without specific prior written permission.
23 **
24 **
25 ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
26 ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
27 ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
28 ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
29 ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
30 ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
31 ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
32 ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
33 ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
34 ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
35 ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
36 **
37 ** $QT_END_LICENSE$
38 **
39 ****************************************************************************/
40
41 /* This script file handles the game logic */
42 .pragma library
43 .import QtQuick.LocalStorage 2.0 as Sql
44 .import "../settings.js" as Settings
45
46 var maxColumn = 10;
47 var maxRow = 13;
48 var types = 3;
49 var maxIndex = maxColumn*maxRow;
50 var board = new Array(maxIndex);
51 var blockSrc = "Block.qml";
52 var gameDuration;
53 var component = Qt.createComponent(blockSrc);
54 var gameCanvas;
55 var betweenTurns = false;
56
57 var puzzleLevel = null;
58 var puzzlePath = "";
59
60 var gameMode = "arcade"; //Set in new game, then tweaks behaviour of other functions
61 var gameOver = false;
62
63 function changeBlock(src)
64 {
65     blockSrc = src;
66     component = Qt.createComponent(blockSrc);
67 }
68
69 // Index function used instead of a 2D array
70 function index(column, row)
71 {
72     return column + row * maxColumn;
73 }
74
75 function timeStr(msecs)
76 {
77     var secs = Math.floor(msecs/1000);
78     var m = Math.floor(secs/60);
79     var ret = "" + m + "m " + (secs%60) + "s";
80     return ret;
81 }
82
83 function cleanUp()
84 {
85     if (gameCanvas == undefined)
86         return;
87     // Delete blocks from previous game
88     for (var i = 0; i < maxIndex; i++) {
89         if (board[i] != null)
90             board[i].destroy();
91         board[i] = null;
92     }
93     if (puzzleLevel != null){
94         puzzleLevel.destroy();
95         puzzleLevel = null;
96     }
97     gameCanvas.mode = ""
98 }
99
100 function startNewGame(gc, mode, map)
101 {
102     gameCanvas = gc;
103     if (mode == undefined)
104         gameMode = "arcade";
105     else
106         gameMode = mode;
107     gameOver = false;
108
109     cleanUp();
110
111     gc.gameOver = false;
112     gc.mode = gameMode;
113     // Calculate board size
114     maxColumn = Math.floor(gameCanvas.width/Settings.blockSize);
115     maxRow = Math.floor(gameCanvas.height/Settings.blockSize);
116     maxIndex = maxRow * maxColumn;
117     if (gameMode == "arcade") //Needs to be after board sizing
118         getHighScore();
119
120
121     // Initialize Board
122     board = new Array(maxIndex);
123     gameCanvas.score = 0;
124     gameCanvas.score2 = 0;
125     gameCanvas.moves = 0;
126     gameCanvas.curTurn = 1;
127     if (gameMode == "puzzle")
128         loadMap(map);
129     else//Note that we load them in reverse order for correct visual stacking
130         for (var column = maxColumn - 1; column >= 0; column--)
131             for (var row = maxRow - 1; row >= 0; row--)
132                 createBlock(column, row);
133     if (gameMode == "puzzle")
134         getLevelHistory();//Needs to be after map load
135     gameDuration = new Date();
136 }
137
138 var fillFound;  // Set after a floodFill call to the number of blocks found
139 var floodBoard; // Set to 1 if the floodFill reaches off that node
140
141 // NOTE: Be careful with vars named x,y, as the calling object's x,y are still in scope
142 function handleClick(x,y)
143 {
144     if (betweenTurns || gameOver || gameCanvas == undefined)
145         return;
146     var column = Math.floor(x/Settings.blockSize);
147     var row = Math.floor(y/Settings.blockSize);
148     if (column >= maxColumn || column < 0 || row >= maxRow || row < 0)
149         return;
150     if (board[index(column, row)] == null)
151         return;
152     // If it's a valid block, remove it and all connected (does nothing if it's not connected)
153     floodFill(column,row, -1);
154     if (fillFound <= 0)
155         return;
156     if (gameMode == "multiplayer" && gameCanvas.curTurn == 2)
157         gameCanvas.score2 += (fillFound - 1) * (fillFound - 1);
158     else
159         gameCanvas.score += (fillFound - 1) * (fillFound - 1);
160     if (gameMode == "multiplayer" && gameCanvas.curTurn == 2)
161         shuffleUp();
162     else
163         shuffleDown();
164     gameCanvas.moves += 1;
165     if (gameMode == "endless")
166         refill();
167     else if (gameMode != "multiplayer")
168         victoryCheck();
169     if (gameMode == "multiplayer" && !gc.gameOver){
170         betweenTurns = true;
171         gameCanvas.swapPlayers();//signal, animate and call turnChange() when ready
172     }
173 }
174
175 function floodFill(column,row,type)
176 {
177     if (board[index(column, row)] == null)
178         return;
179     var first = false;
180     if (type == -1) {
181         first = true;
182         type = board[index(column,row)].type;
183
184         // Flood fill initialization
185         fillFound = 0;
186         floodBoard = new Array(maxIndex);
187     }
188     if (column >= maxColumn || column < 0 || row >= maxRow || row < 0)
189         return;
190     if (floodBoard[index(column, row)] == 1 || (!first && type != board[index(column, row)].type))
191         return;
192     floodBoard[index(column, row)] = 1;
193     floodFill(column + 1, row, type);
194     floodFill(column - 1, row, type);
195     floodFill(column, row + 1, type);
196     floodFill(column, row - 1, type);
197     if (first == true && fillFound == 0)
198         return; // Can't remove single blocks
199     board[index(column, row)].dying = true;
200     board[index(column, row)] = null;
201     fillFound += 1;
202 }
203
204 function shuffleDown()
205 {
206     // Fall down
207     for (var column = 0; column < maxColumn; column++) {
208         var fallDist = 0;
209         for (var row = maxRow - 1; row >= 0; row--) {
210             if (board[index(column,row)] == null) {
211                 fallDist += 1;
212             } else {
213                 if (fallDist > 0) {
214                     var obj = board[index(column, row)];
215                     obj.y = (row + fallDist) * Settings.blockSize;
216                     board[index(column, row + fallDist)] = obj;
217                     board[index(column, row)] = null;
218                 }
219             }
220         }
221     }
222     // Fall to the left
223     fallDist = 0;
224     for (column = 0; column < maxColumn; column++) {
225         if (board[index(column, maxRow - 1)] == null) {
226             fallDist += 1;
227         } else {
228             if (fallDist > 0) {
229                 for (row = 0; row < maxRow; row++) {
230                     obj = board[index(column, row)];
231                     if (obj == null)
232                         continue;
233                     obj.x = (column - fallDist) * Settings.blockSize;
234                     board[index(column - fallDist,row)] = obj;
235                     board[index(column, row)] = null;
236                 }
237             }
238         }
239     }
240 }
241
242
243 function shuffleUp()
244 {
245     // Fall up
246     for (var column = 0; column < maxColumn; column++) {
247         var fallDist = 0;
248         for (var row = 0; row < maxRow; row++) {
249             if (board[index(column,row)] == null) {
250                 fallDist += 1;
251             } else {
252                 if (fallDist > 0) {
253                     var obj = board[index(column, row)];
254                     obj.y = (row - fallDist) * Settings.blockSize;
255                     board[index(column, row - fallDist)] = obj;
256                     board[index(column, row)] = null;
257                 }
258             }
259         }
260     }
261     // Fall to the left (or should it be right, so as to be left for P2?)
262     fallDist = 0;
263     for (column = 0; column < maxColumn; column++) {
264         if (board[index(column, 0)] == null) {
265             fallDist += 1;
266         } else {
267             if (fallDist > 0) {
268                 for (row = 0; row < maxRow; row++) {
269                     obj = board[index(column, row)];
270                     if (obj == null)
271                         continue;
272                     obj.x = (column - fallDist) * Settings.blockSize;
273                     board[index(column - fallDist,row)] = obj;
274                     board[index(column, row)] = null;
275                 }
276             }
277         }
278     }
279 }
280
281 function turnChange()//called by ui outside
282 {
283     betweenTurns = false;
284     if (gameCanvas.curTurn == 1){
285         shuffleUp();
286         gameCanvas.curTurn = 2;
287         victoryCheck();
288     }else{
289         shuffleDown();
290         gameCanvas.curTurn = 1;
291         victoryCheck();
292     }
293 }
294
295 function refill()
296 {
297     for (var column = 0; column < maxColumn; column++) {
298         for (var row = 0; row < maxRow; row++) {
299             if (board[index(column, row)] == null)
300                 createBlock(column, row);
301         }
302     }
303 }
304
305 function victoryCheck()
306 {
307     // Awards bonuses for no blocks left
308     var deservesBonus = true;
309     if (board[index(0,maxRow - 1)] != null || board[index(0,0)] != null)
310         deservesBonus = false;
311     // Checks for game over
312     if (deservesBonus){
313         if (gameCanvas.curTurn = 1)
314             gameCanvas.score += 1000;
315         else
316             gameCanvas.score2 += 1000;
317     }
318     gameOver = deservesBonus;
319     if (gameCanvas.curTurn == 1){
320         if (!(floodMoveCheck(0, maxRow - 1, -1)))
321             gameOver = true;
322     }else{
323         if (!(floodMoveCheck(0, 0, -1, true)))
324             gameOver = true;
325     }
326     if (gameMode == "puzzle"){
327         puzzleVictoryCheck(deservesBonus);//Takes it from here
328         return;
329     }
330     if (gameOver) {
331         var winnerScore = Math.max(gameCanvas.score, gameCanvas.score2);
332         if (gameMode == "multiplayer"){
333             gameCanvas.score = winnerScore;
334             saveHighScore(gameCanvas.score2);
335         }
336         saveHighScore(gameCanvas.score);
337         gameDuration = new Date() - gameDuration;
338         gameCanvas.gameOver = true;
339     }
340 }
341
342 // Only floods up and right, to see if it can find adjacent same-typed blocks
343 function floodMoveCheck(column, row, type, goDownInstead)
344 {
345     if (column >= maxColumn || column < 0 || row >= maxRow || row < 0)
346         return false;
347     if (board[index(column, row)] == null)
348         return false;
349     var myType = board[index(column, row)].type;
350     if (type == myType)
351         return true;
352     if (goDownInstead)
353         return floodMoveCheck(column + 1, row, myType, goDownInstead) ||
354                floodMoveCheck(column, row + 1, myType, goDownInstead);
355     else
356         return floodMoveCheck(column + 1, row, myType) ||
357                floodMoveCheck(column, row - 1, myType);
358 }
359
360 function createBlock(column,row,type)
361 {
362     // Note that we don't wait for the component to become ready. This will
363     // only work if the block QML is a local file. Otherwise the component will
364     // not be ready immediately. There is a statusChanged signal on the
365     // component you could use if you want to wait to load remote files.
366     if (component.status == 1){
367         if (type == undefined)
368             type = Math.floor(Math.random() * types);
369         if (type < 0 || type > 4) {
370             console.log("Invalid type requested");//TODO: Is this triggered by custom levels much?
371             return;
372         }
373         var dynamicObject = component.createObject(gameCanvas,
374                 {"type": type,
375                 "x": column*Settings.blockSize,
376                 "y": -1*Settings.blockSize,
377                 "width": Settings.blockSize,
378                 "height": Settings.blockSize,
379                 "particleSystem": gameCanvas.ps});
380         if (dynamicObject == null){
381             console.log("error creating block");
382             console.log(component.errorString());
383             return false;
384         }
385         dynamicObject.y = row*Settings.blockSize;
386         dynamicObject.spawned = true;
387
388         board[index(column,row)] = dynamicObject;
389     }else{
390         console.log("error loading block component");
391         console.log(component.errorString());
392         return false;
393     }
394     return true;
395 }
396
397 function showPuzzleError(str)
398 {
399     //TODO: Nice user visible UI?
400     console.log(str);
401 }
402
403 function loadMap(map)
404 {
405     puzzlePath = map;
406     var levelComp = Qt.createComponent(puzzlePath);
407     if (levelComp.status != 1){
408         console.log("Error loading level");
409         showPuzzleError(levelComp.errorString());
410         return;
411     }
412     puzzleLevel = levelComp.createObject();
413     if (puzzleLevel == null || !puzzleLevel.startingGrid instanceof Array) {
414         showPuzzleError("Bugger!");
415         return;
416     }
417     gameCanvas.showPuzzleGoal(puzzleLevel.goalText);
418     //showPuzzleGoal should call finishLoadingMap as the next thing it does, before handling more events
419 }
420
421 function finishLoadingMap()
422 {
423     for (var i in puzzleLevel.startingGrid)
424         if (! (puzzleLevel.startingGrid[i] >= 0 && puzzleLevel.startingGrid[i] <= 9) )
425             puzzleLevel.startingGrid[i] = 0;
426     //TODO: Don't allow loading larger levels, leads to cheating
427     while (puzzleLevel.startingGrid.length > maxIndex) puzzleLevel.startingGrid.shift();
428     while (puzzleLevel.startingGrid.length < maxIndex) puzzleLevel.startingGrid.unshift(0);
429     for (var i in puzzleLevel.startingGrid)
430         if (puzzleLevel.startingGrid[i] > 0)
431             createBlock(i % maxColumn, Math.floor(i / maxColumn), puzzleLevel.startingGrid[i] - 1);
432
433     //### Experimental feature - allow levels to contain arbitrary QML scenes as well!
434     //while (puzzleLevel.children.length)
435     //    puzzleLevel.children[0].parent = gameCanvas;
436     gameDuration = new Date(); //Don't start until we finish loading
437 }
438
439 function puzzleVictoryCheck(clearedAll)//gameOver has also been set if no more moves
440 {
441     var won = true;
442     var soFar = new Date() - gameDuration;
443     if (puzzleLevel.scoreTarget != -1 && gameCanvas.score < puzzleLevel.scoreTarget){
444         won = false;
445     } if (puzzleLevel.scoreTarget != -1 && gameCanvas.score >= puzzleLevel.scoreTarget && !puzzleLevel.mustClear){
446         gameOver = true;
447     } if (puzzleLevel.timeTarget != -1 && soFar/1000.0 > puzzleLevel.timeTarget){
448         gameOver = true;
449     } if (puzzleLevel.moveTarget != -1 && gameCanvas.moves >= puzzleLevel.moveTarget){
450         gameOver = true;
451     } if (puzzleLevel.mustClear && gameOver && !clearedAll) {
452         won = false;
453     }
454
455     if (gameOver) {
456         gameCanvas.gameOver = true;
457         gameCanvas.showPuzzleEnd(won);
458
459         if (won) {
460             // Store progress
461             saveLevelHistory();
462         }
463     }
464 }
465
466 function getHighScore()
467 {
468     var db = Sql.LocalStorage.openDatabaseSync(
469         "SameGame",
470         "2.0",
471         "SameGame Local Data",
472         100
473     );
474     db.transaction(
475         function(tx) {
476             tx.executeSql('CREATE TABLE IF NOT EXISTS Scores(game TEXT, score NUMBER, gridSize TEXT, time NUMBER)');
477             // Only show results for the current grid size
478             var rs = tx.executeSql('SELECT * FROM Scores WHERE gridSize = "'
479                 + maxColumn + "x" + maxRow + '" AND game = "' + gameMode + '" ORDER BY score desc');
480             if (rs.rows.length > 0)
481                 gameCanvas.highScore = rs.rows.item(0).score;
482             else
483                 gameCanvas.highScore = 0;
484         }
485     );
486 }
487
488 function saveHighScore(score)
489 {
490     // Offline storage
491     var db = Sql.LocalStorage.openDatabaseSync(
492         "SameGame",
493         "2.0",
494         "SameGame Local Data",
495         100
496     );
497     var dataStr = "INSERT INTO Scores VALUES(?, ?, ?, ?)";
498     var data = [
499         gameMode,
500         score,
501         maxColumn + "x" + maxRow,
502         Math.floor(gameDuration / 1000)
503     ];
504     if (score >= gameCanvas.highScore)//Update UI field
505         gameCanvas.highScore = score;
506
507     db.transaction(
508         function(tx) {
509             tx.executeSql('CREATE TABLE IF NOT EXISTS Scores(game TEXT, score NUMBER, gridSize TEXT, time NUMBER)');
510             tx.executeSql(dataStr, data);
511         }
512     );
513 }
514
515 function getLevelHistory()
516 {
517     var db = Sql.LocalStorage.openDatabaseSync(
518         "SameGame",
519         "2.0",
520         "SameGame Local Data",
521         100
522     );
523     db.transaction(
524         function(tx) {
525             tx.executeSql('CREATE TABLE IF NOT EXISTS Puzzle(level TEXT, score NUMBER, moves NUMBER, time NUMBER)');
526             var rs = tx.executeSql('SELECT * FROM Puzzle WHERE level = "' + puzzlePath + '" ORDER BY score desc');
527             if (rs.rows.length > 0) {
528                 gameCanvas.puzzleWon = true;
529                 gameCanvas.highScore = rs.rows.item(0).score;
530             } else {
531                 gameCanvas.puzzleWon = false;
532                 gameCanvas.highScore = 0;
533             }
534         }
535     );
536 }
537
538 function saveLevelHistory()
539 {
540     var db = Sql.LocalStorage.openDatabaseSync(
541         "SameGame",
542         "2.0",
543         "SameGame Local Data",
544         100
545     );
546     var dataStr = "INSERT INTO Puzzle VALUES(?, ?, ?, ?)";
547     var data = [
548         puzzlePath,
549         gameCanvas.score,
550         gameCanvas.moves,
551         Math.floor(gameDuration / 1000)
552     ];
553     gameCanvas.puzzleWon = true;
554
555     db.transaction(
556         function(tx) {
557             tx.executeSql('CREATE TABLE IF NOT EXISTS Puzzle(level TEXT, score NUMBER, moves NUMBER, time NUMBER)');
558             tx.executeSql(dataStr, data);
559         }
560     );
561 }
562
563 function nuke() //For "Debug mode"
564 {
565     for (var row = 1; row <= 5; row++) {
566         for (var col = 0; col < 5; col++) {
567             if (board[index(col, maxRow - row)] != null) {
568                 board[index(col, maxRow - row)].dying = true;
569                 board[index(col, maxRow - row)] = null;
570             }
571         }
572     }
573     if (gameMode == "multiplayer" && gameCanvas.curTurn == 2)
574         shuffleUp();
575     else
576         shuffleDown();
577     if (gameMode == "endless")
578         refill();
579     else
580         victoryCheck();
581 }