1 /****************************************************************************
3 ** Copyright (C) 2012 Digia Plc and/or its subsidiary(-ies).
4 ** Contact: http://www.qt-project.org/legal
6 ** This file is part of the examples of the Qt Toolkit.
8 ** $QT_BEGIN_LICENSE:BSD$
9 ** You may use this file under the terms of the BSD license as follows:
11 ** "Redistribution and use in source and binary forms, with or without
12 ** modification, are permitted provided that the following conditions are
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
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.
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."
39 ****************************************************************************/
41 /* This script file handles the game logic */
43 .import QtQuick.LocalStorage 2.0 as Sql
44 .import "../settings.js" as Settings
49 var maxIndex = maxColumn*maxRow;
50 var board = new Array(maxIndex);
51 var blockSrc = "Block.qml";
53 var component = Qt.createComponent(blockSrc);
55 var betweenTurns = false;
57 var puzzleLevel = null;
60 var gameMode = "arcade"; //Set in new game, then tweaks behaviour of other functions
63 function changeBlock(src)
66 component = Qt.createComponent(blockSrc);
69 // Index function used instead of a 2D array
70 function index(column, row)
72 return column + row * maxColumn;
75 function timeStr(msecs)
77 var secs = Math.floor(msecs/1000);
78 var m = Math.floor(secs/60);
79 var ret = "" + m + "m " + (secs%60) + "s";
85 if (gameCanvas == undefined)
87 // Delete blocks from previous game
88 for (var i = 0; i < maxIndex; i++) {
93 if (puzzleLevel != null){
94 puzzleLevel.destroy();
100 function startNewGame(gc, mode, map)
103 if (mode == undefined)
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
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")
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();
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
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)
144 if (betweenTurns || gameOver || gameCanvas == undefined)
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)
150 if (board[index(column, row)] == null)
152 // If it's a valid block, remove it and all connected (does nothing if it's not connected)
153 floodFill(column,row, -1);
156 if (gameMode == "multiplayer" && gameCanvas.curTurn == 2)
157 gameCanvas.score2 += (fillFound - 1) * (fillFound - 1);
159 gameCanvas.score += (fillFound - 1) * (fillFound - 1);
160 if (gameMode == "multiplayer" && gameCanvas.curTurn == 2)
164 gameCanvas.moves += 1;
165 if (gameMode == "endless")
167 else if (gameMode != "multiplayer")
169 if (gameMode == "multiplayer" && !gc.gameOver){
171 gameCanvas.swapPlayers();//signal, animate and call turnChange() when ready
175 function floodFill(column,row,type)
177 if (board[index(column, row)] == null)
182 type = board[index(column,row)].type;
184 // Flood fill initialization
186 floodBoard = new Array(maxIndex);
188 if (column >= maxColumn || column < 0 || row >= maxRow || row < 0)
190 if (floodBoard[index(column, row)] == 1 || (!first && type != board[index(column, row)].type))
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;
204 function shuffleDown()
207 for (var column = 0; column < maxColumn; column++) {
209 for (var row = maxRow - 1; row >= 0; row--) {
210 if (board[index(column,row)] == null) {
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;
224 for (column = 0; column < maxColumn; column++) {
225 if (board[index(column, maxRow - 1)] == null) {
229 for (row = 0; row < maxRow; row++) {
230 obj = board[index(column, row)];
233 obj.x = (column - fallDist) * Settings.blockSize;
234 board[index(column - fallDist,row)] = obj;
235 board[index(column, row)] = null;
246 for (var column = 0; column < maxColumn; column++) {
248 for (var row = 0; row < maxRow; row++) {
249 if (board[index(column,row)] == null) {
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;
261 // Fall to the left (or should it be right, so as to be left for P2?)
263 for (column = 0; column < maxColumn; column++) {
264 if (board[index(column, 0)] == null) {
268 for (row = 0; row < maxRow; row++) {
269 obj = board[index(column, row)];
272 obj.x = (column - fallDist) * Settings.blockSize;
273 board[index(column - fallDist,row)] = obj;
274 board[index(column, row)] = null;
281 function turnChange()//called by ui outside
283 betweenTurns = false;
284 if (gameCanvas.curTurn == 1){
286 gameCanvas.curTurn = 2;
290 gameCanvas.curTurn = 1;
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);
305 function victoryCheck()
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
313 if (gameCanvas.curTurn = 1)
314 gameCanvas.score += 1000;
316 gameCanvas.score2 += 1000;
318 gameOver = deservesBonus;
319 if (gameCanvas.curTurn == 1){
320 if (!(floodMoveCheck(0, maxRow - 1, -1)))
323 if (!(floodMoveCheck(0, 0, -1, true)))
326 if (gameMode == "puzzle"){
327 puzzleVictoryCheck(deservesBonus);//Takes it from here
331 var winnerScore = Math.max(gameCanvas.score, gameCanvas.score2);
332 if (gameMode == "multiplayer"){
333 gameCanvas.score = winnerScore;
334 saveHighScore(gameCanvas.score2);
336 saveHighScore(gameCanvas.score);
337 gameDuration = new Date() - gameDuration;
338 gameCanvas.gameOver = true;
342 // Only floods up and right, to see if it can find adjacent same-typed blocks
343 function floodMoveCheck(column, row, type, goDownInstead)
345 if (column >= maxColumn || column < 0 || row >= maxRow || row < 0)
347 if (board[index(column, row)] == null)
349 var myType = board[index(column, row)].type;
353 return floodMoveCheck(column + 1, row, myType, goDownInstead) ||
354 floodMoveCheck(column, row + 1, myType, goDownInstead);
356 return floodMoveCheck(column + 1, row, myType) ||
357 floodMoveCheck(column, row - 1, myType);
360 function createBlock(column,row,type)
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?
373 var dynamicObject = component.createObject(gameCanvas,
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());
385 dynamicObject.y = row*Settings.blockSize;
386 dynamicObject.spawned = true;
388 board[index(column,row)] = dynamicObject;
390 console.log("error loading block component");
391 console.log(component.errorString());
397 function showPuzzleError(str)
399 //TODO: Nice user visible UI?
403 function loadMap(map)
406 var levelComp = Qt.createComponent(puzzlePath);
407 if (levelComp.status != 1){
408 console.log("Error loading level");
409 showPuzzleError(levelComp.errorString());
412 puzzleLevel = levelComp.createObject();
413 if (puzzleLevel == null || !puzzleLevel.startingGrid instanceof Array) {
414 showPuzzleError("Bugger!");
417 gameCanvas.showPuzzleGoal(puzzleLevel.goalText);
418 //showPuzzleGoal should call finishLoadingMap as the next thing it does, before handling more events
421 function finishLoadingMap()
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);
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
439 function puzzleVictoryCheck(clearedAll)//gameOver has also been set if no more moves
442 var soFar = new Date() - gameDuration;
443 if (puzzleLevel.scoreTarget != -1 && gameCanvas.score < puzzleLevel.scoreTarget){
445 } if (puzzleLevel.scoreTarget != -1 && gameCanvas.score >= puzzleLevel.scoreTarget && !puzzleLevel.mustClear){
447 } if (puzzleLevel.timeTarget != -1 && soFar/1000.0 > puzzleLevel.timeTarget){
449 } if (puzzleLevel.moveTarget != -1 && gameCanvas.moves >= puzzleLevel.moveTarget){
451 } if (puzzleLevel.mustClear && gameOver && !clearedAll) {
456 gameCanvas.gameOver = true;
457 gameCanvas.showPuzzleEnd(won);
466 function getHighScore()
468 var db = Sql.LocalStorage.openDatabaseSync(
471 "SameGame Local Data",
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;
483 gameCanvas.highScore = 0;
488 function saveHighScore(score)
491 var db = Sql.LocalStorage.openDatabaseSync(
494 "SameGame Local Data",
497 var dataStr = "INSERT INTO Scores VALUES(?, ?, ?, ?)";
501 maxColumn + "x" + maxRow,
502 Math.floor(gameDuration / 1000)
504 if (score >= gameCanvas.highScore)//Update UI field
505 gameCanvas.highScore = score;
509 tx.executeSql('CREATE TABLE IF NOT EXISTS Scores(game TEXT, score NUMBER, gridSize TEXT, time NUMBER)');
510 tx.executeSql(dataStr, data);
515 function getLevelHistory()
517 var db = Sql.LocalStorage.openDatabaseSync(
520 "SameGame Local Data",
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;
531 gameCanvas.puzzleWon = false;
532 gameCanvas.highScore = 0;
538 function saveLevelHistory()
540 var db = Sql.LocalStorage.openDatabaseSync(
543 "SameGame Local Data",
546 var dataStr = "INSERT INTO Puzzle VALUES(?, ?, ?, ?)";
551 Math.floor(gameDuration / 1000)
553 gameCanvas.puzzleWon = true;
557 tx.executeSql('CREATE TABLE IF NOT EXISTS Puzzle(level TEXT, score NUMBER, moves NUMBER, time NUMBER)');
558 tx.executeSql(dataStr, data);
563 function nuke() //For "Debug mode"
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;
573 if (gameMode == "multiplayer" && gameCanvas.curTurn == 2)
577 if (gameMode == "endless")