| /**************************************************************************** |
| ** |
| ** Copyright (C) 2017 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the examples of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:BSD$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** BSD License Usage |
| ** Alternatively, you may use this file under the terms of the BSD license |
| ** as follows: |
| ** |
| ** "Redistribution and use in source and binary forms, with or without |
| ** modification, are permitted provided that the following conditions are |
| ** met: |
| ** * Redistributions of source code must retain the above copyright |
| ** notice, this list of conditions and the following disclaimer. |
| ** * Redistributions in binary form must reproduce the above copyright |
| ** notice, this list of conditions and the following disclaimer in |
| ** the documentation and/or other materials provided with the |
| ** distribution. |
| ** * Neither the name of The Qt Company Ltd nor the names of its |
| ** contributors may be used to endorse or promote products derived |
| ** from this software without specific prior written permission. |
| ** |
| ** |
| ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| /* This script file handles the game logic */ |
| .pragma library |
| .import QtQuick.LocalStorage 2.0 as Sql |
| |
| var maxColumn = 10; |
| var maxRow = 13; |
| var types = 3; |
| var maxIndex = maxColumn*maxRow; |
| var board = new Array(maxIndex); |
| var blockSrc = "Block.qml"; |
| var gameDuration; |
| var component = Qt.createComponent(blockSrc); |
| var gameCanvas; |
| var betweenTurns = false; |
| |
| var puzzleLevel = null; |
| var puzzlePath = ""; |
| |
| var gameMode = "arcade"; //Set in new game, then tweaks behaviour of other functions |
| var gameOver = false; |
| |
| function changeBlock(src) |
| { |
| blockSrc = src; |
| component = Qt.createComponent(blockSrc); |
| } |
| |
| // Index function used instead of a 2D array |
| function index(column, row) |
| { |
| return column + row * maxColumn; |
| } |
| |
| function timeStr(msecs) |
| { |
| var secs = Math.floor(msecs/1000); |
| var m = Math.floor(secs/60); |
| var ret = "" + m + "m " + (secs%60) + "s"; |
| return ret; |
| } |
| |
| function cleanUp() |
| { |
| if (gameCanvas == undefined) |
| return; |
| // Delete blocks from previous game |
| for (var i = 0; i < maxIndex; i++) { |
| if (board[i] != null) |
| board[i].destroy(); |
| board[i] = null; |
| } |
| if (puzzleLevel != null){ |
| puzzleLevel.destroy(); |
| puzzleLevel = null; |
| } |
| gameCanvas.mode = "" |
| } |
| |
| function startNewGame(gc, mode, map) |
| { |
| gameCanvas = gc; |
| if (mode == undefined) |
| gameMode = "arcade"; |
| else |
| gameMode = mode; |
| gameOver = false; |
| |
| cleanUp(); |
| |
| gc.gameOver = false; |
| gc.mode = gameMode; |
| // Calculate board size |
| maxColumn = Math.floor(gameCanvas.width/gameCanvas.blockSize); |
| maxRow = Math.floor(gameCanvas.height/gameCanvas.blockSize); |
| maxIndex = maxRow * maxColumn; |
| if (gameMode == "arcade") //Needs to be after board sizing |
| getHighScore(); |
| |
| |
| // Initialize Board |
| board = new Array(maxIndex); |
| gameCanvas.score = 0; |
| gameCanvas.score2 = 0; |
| gameCanvas.moves = 0; |
| gameCanvas.curTurn = 1; |
| if (gameMode == "puzzle") |
| loadMap(map); |
| else//Note that we load them in reverse order for correct visual stacking |
| for (var column = maxColumn - 1; column >= 0; column--) |
| for (var row = maxRow - 1; row >= 0; row--) |
| createBlock(column, row); |
| if (gameMode == "puzzle") |
| getLevelHistory();//Needs to be after map load |
| gameDuration = new Date(); |
| } |
| |
| var fillFound; // Set after a floodFill call to the number of blocks found |
| var floodBoard; // Set to 1 if the floodFill reaches off that node |
| |
| // NOTE: Be careful with vars named x,y, as the calling object's x,y are still in scope |
| function handleClick(x,y) |
| { |
| if (betweenTurns || gameOver || gameCanvas == undefined) |
| return; |
| var column = Math.floor(x/gameCanvas.blockSize); |
| var row = Math.floor(y/gameCanvas.blockSize); |
| if (column >= maxColumn || column < 0 || row >= maxRow || row < 0) |
| return; |
| if (board[index(column, row)] == null) |
| return; |
| // If it's a valid block, remove it and all connected (does nothing if it's not connected) |
| floodFill(column,row, -1); |
| if (fillFound <= 0) |
| return; |
| if (gameMode == "multiplayer" && gameCanvas.curTurn == 2) |
| gameCanvas.score2 += (fillFound - 1) * (fillFound - 1); |
| else |
| gameCanvas.score += (fillFound - 1) * (fillFound - 1); |
| if (gameMode == "multiplayer" && gameCanvas.curTurn == 2) |
| shuffleUp(); |
| else |
| shuffleDown(); |
| gameCanvas.moves += 1; |
| if (gameMode == "endless") |
| refill(); |
| else if (gameMode != "multiplayer") |
| victoryCheck(); |
| if (gameMode == "multiplayer" && !gc.gameOver){ |
| betweenTurns = true; |
| gameCanvas.swapPlayers();//signal, animate and call turnChange() when ready |
| } |
| } |
| |
| function floodFill(column,row,type) |
| { |
| if (board[index(column, row)] == null) |
| return; |
| var first = false; |
| if (type == -1) { |
| first = true; |
| type = board[index(column,row)].type; |
| |
| // Flood fill initialization |
| fillFound = 0; |
| floodBoard = new Array(maxIndex); |
| } |
| if (column >= maxColumn || column < 0 || row >= maxRow || row < 0) |
| return; |
| if (floodBoard[index(column, row)] == 1 || (!first && type != board[index(column, row)].type)) |
| return; |
| floodBoard[index(column, row)] = 1; |
| floodFill(column + 1, row, type); |
| floodFill(column - 1, row, type); |
| floodFill(column, row + 1, type); |
| floodFill(column, row - 1, type); |
| if (first == true && fillFound == 0) |
| return; // Can't remove single blocks |
| board[index(column, row)].dying = true; |
| board[index(column, row)] = null; |
| fillFound += 1; |
| } |
| |
| function shuffleDown() |
| { |
| // Fall down |
| for (var column = 0; column < maxColumn; column++) { |
| var fallDist = 0; |
| for (var row = maxRow - 1; row >= 0; row--) { |
| if (board[index(column,row)] == null) { |
| fallDist += 1; |
| } else { |
| if (fallDist > 0) { |
| var obj = board[index(column, row)]; |
| obj.y = (row + fallDist) * gameCanvas.blockSize; |
| board[index(column, row + fallDist)] = obj; |
| board[index(column, row)] = null; |
| } |
| } |
| } |
| } |
| // Fall to the left |
| fallDist = 0; |
| for (column = 0; column < maxColumn; column++) { |
| if (board[index(column, maxRow - 1)] == null) { |
| fallDist += 1; |
| } else { |
| if (fallDist > 0) { |
| for (row = 0; row < maxRow; row++) { |
| obj = board[index(column, row)]; |
| if (obj == null) |
| continue; |
| obj.x = (column - fallDist) * gameCanvas.blockSize; |
| board[index(column - fallDist,row)] = obj; |
| board[index(column, row)] = null; |
| } |
| } |
| } |
| } |
| } |
| |
| |
| function shuffleUp() |
| { |
| // Fall up |
| for (var column = 0; column < maxColumn; column++) { |
| var fallDist = 0; |
| for (var row = 0; row < maxRow; row++) { |
| if (board[index(column,row)] == null) { |
| fallDist += 1; |
| } else { |
| if (fallDist > 0) { |
| var obj = board[index(column, row)]; |
| obj.y = (row - fallDist) * gameCanvas.blockSize; |
| board[index(column, row - fallDist)] = obj; |
| board[index(column, row)] = null; |
| } |
| } |
| } |
| } |
| // Fall to the left (or should it be right, so as to be left for P2?) |
| fallDist = 0; |
| for (column = 0; column < maxColumn; column++) { |
| if (board[index(column, 0)] == null) { |
| fallDist += 1; |
| } else { |
| if (fallDist > 0) { |
| for (row = 0; row < maxRow; row++) { |
| obj = board[index(column, row)]; |
| if (obj == null) |
| continue; |
| obj.x = (column - fallDist) * gameCanvas.blockSize; |
| board[index(column - fallDist,row)] = obj; |
| board[index(column, row)] = null; |
| } |
| } |
| } |
| } |
| } |
| |
| function turnChange()//called by ui outside |
| { |
| betweenTurns = false; |
| if (gameCanvas.curTurn == 1){ |
| shuffleUp(); |
| gameCanvas.curTurn = 2; |
| victoryCheck(); |
| }else{ |
| shuffleDown(); |
| gameCanvas.curTurn = 1; |
| victoryCheck(); |
| } |
| } |
| |
| function refill() |
| { |
| for (var column = 0; column < maxColumn; column++) { |
| for (var row = 0; row < maxRow; row++) { |
| if (board[index(column, row)] == null) |
| createBlock(column, row); |
| } |
| } |
| } |
| |
| function victoryCheck() |
| { |
| // Awards bonuses for no blocks left |
| var deservesBonus = true; |
| if (board[index(0,maxRow - 1)] != null || board[index(0,0)] != null) |
| deservesBonus = false; |
| // Checks for game over |
| if (deservesBonus){ |
| if (gameCanvas.curTurn = 1) |
| gameCanvas.score += 1000; |
| else |
| gameCanvas.score2 += 1000; |
| } |
| gameOver = deservesBonus; |
| if (gameCanvas.curTurn == 1){ |
| if (!(floodMoveCheck(0, maxRow - 1, -1))) |
| gameOver = true; |
| }else{ |
| if (!(floodMoveCheck(0, 0, -1, true))) |
| gameOver = true; |
| } |
| if (gameMode == "puzzle"){ |
| puzzleVictoryCheck(deservesBonus);//Takes it from here |
| return; |
| } |
| if (gameOver) { |
| var winnerScore = Math.max(gameCanvas.score, gameCanvas.score2); |
| if (gameMode == "multiplayer"){ |
| gameCanvas.score = winnerScore; |
| saveHighScore(gameCanvas.score2); |
| } |
| saveHighScore(gameCanvas.score); |
| gameDuration = new Date() - gameDuration; |
| gameCanvas.gameOver = true; |
| } |
| } |
| |
| // Only floods up and right, to see if it can find adjacent same-typed blocks |
| function floodMoveCheck(column, row, type, goDownInstead) |
| { |
| if (column >= maxColumn || column < 0 || row >= maxRow || row < 0) |
| return false; |
| if (board[index(column, row)] == null) |
| return false; |
| var myType = board[index(column, row)].type; |
| if (type == myType) |
| return true; |
| if (goDownInstead) |
| return floodMoveCheck(column + 1, row, myType, goDownInstead) || |
| floodMoveCheck(column, row + 1, myType, goDownInstead); |
| else |
| return floodMoveCheck(column + 1, row, myType) || |
| floodMoveCheck(column, row - 1, myType); |
| } |
| |
| function createBlock(column,row,type) |
| { |
| // Note that we don't wait for the component to become ready. This will |
| // only work if the block QML is a local file. Otherwise the component will |
| // not be ready immediately. There is a statusChanged signal on the |
| // component you could use if you want to wait to load remote files. |
| if (component.status == 1){ |
| if (type == undefined) |
| type = Math.floor(Math.random() * types); |
| if (type < 0 || type > 4) { |
| console.log("Invalid type requested");//TODO: Is this triggered by custom levels much? |
| return; |
| } |
| var dynamicObject = component.createObject(gameCanvas, |
| {"type": type, |
| "x": column*gameCanvas.blockSize, |
| "y": -1*gameCanvas.blockSize, |
| "width": gameCanvas.blockSize, |
| "height": gameCanvas.blockSize, |
| "particleSystem": gameCanvas.ps}); |
| if (dynamicObject == null){ |
| console.log("error creating block"); |
| console.log(component.errorString()); |
| return false; |
| } |
| dynamicObject.y = row*gameCanvas.blockSize; |
| dynamicObject.spawned = true; |
| |
| board[index(column,row)] = dynamicObject; |
| }else{ |
| console.log("error loading block component"); |
| console.log(component.errorString()); |
| return false; |
| } |
| return true; |
| } |
| |
| function showPuzzleError(str) |
| { |
| //TODO: Nice user visible UI? |
| console.log(str); |
| } |
| |
| function loadMap(map) |
| { |
| puzzlePath = map; |
| var levelComp = Qt.createComponent(puzzlePath); |
| if (levelComp.status != 1){ |
| console.log("Error loading level"); |
| showPuzzleError(levelComp.errorString()); |
| return; |
| } |
| puzzleLevel = levelComp.createObject(); |
| if (puzzleLevel == null || !puzzleLevel.startingGrid instanceof Array) { |
| showPuzzleError("Bugger!"); |
| return; |
| } |
| gameCanvas.showPuzzleGoal(puzzleLevel.goalText); |
| //showPuzzleGoal should call finishLoadingMap as the next thing it does, before handling more events |
| } |
| |
| function finishLoadingMap() |
| { |
| for (var i in puzzleLevel.startingGrid) |
| if (! (puzzleLevel.startingGrid[i] >= 0 && puzzleLevel.startingGrid[i] <= 9) ) |
| puzzleLevel.startingGrid[i] = 0; |
| //TODO: Don't allow loading larger levels, leads to cheating |
| while (puzzleLevel.startingGrid.length > maxIndex) puzzleLevel.startingGrid.shift(); |
| while (puzzleLevel.startingGrid.length < maxIndex) puzzleLevel.startingGrid.unshift(0); |
| for (var i in puzzleLevel.startingGrid) |
| if (puzzleLevel.startingGrid[i] > 0) |
| createBlock(i % maxColumn, Math.floor(i / maxColumn), puzzleLevel.startingGrid[i] - 1); |
| |
| //### Experimental feature - allow levels to contain arbitrary QML scenes as well! |
| //while (puzzleLevel.children.length) |
| // puzzleLevel.children[0].parent = gameCanvas; |
| gameDuration = new Date(); //Don't start until we finish loading |
| } |
| |
| function puzzleVictoryCheck(clearedAll)//gameOver has also been set if no more moves |
| { |
| var won = true; |
| var soFar = new Date() - gameDuration; |
| if (puzzleLevel.scoreTarget != -1 && gameCanvas.score < puzzleLevel.scoreTarget){ |
| won = false; |
| } if (puzzleLevel.scoreTarget != -1 && gameCanvas.score >= puzzleLevel.scoreTarget && !puzzleLevel.mustClear){ |
| gameOver = true; |
| } if (puzzleLevel.timeTarget != -1 && soFar/1000.0 > puzzleLevel.timeTarget){ |
| gameOver = true; |
| } if (puzzleLevel.moveTarget != -1 && gameCanvas.moves >= puzzleLevel.moveTarget){ |
| gameOver = true; |
| } if (puzzleLevel.mustClear && gameOver && !clearedAll) { |
| won = false; |
| } |
| |
| if (gameOver) { |
| gameCanvas.gameOver = true; |
| gameCanvas.showPuzzleEnd(won); |
| |
| if (won) { |
| // Store progress |
| saveLevelHistory(); |
| } |
| } |
| } |
| |
| function getHighScore() |
| { |
| var db = Sql.LocalStorage.openDatabaseSync( |
| "SameGame", |
| "2.0", |
| "SameGame Local Data", |
| 100 |
| ); |
| db.transaction( |
| function(tx) { |
| tx.executeSql('CREATE TABLE IF NOT EXISTS Scores(game TEXT, score NUMBER, gridSize TEXT, time NUMBER)'); |
| // Only show results for the current grid size |
| var rs = tx.executeSql('SELECT * FROM Scores WHERE gridSize = "' |
| + maxColumn + "x" + maxRow + '" AND game = "' + gameMode + '" ORDER BY score desc'); |
| if (rs.rows.length > 0) |
| gameCanvas.highScore = rs.rows.item(0).score; |
| else |
| gameCanvas.highScore = 0; |
| } |
| ); |
| } |
| |
| function saveHighScore(score) |
| { |
| // Offline storage |
| var db = Sql.LocalStorage.openDatabaseSync( |
| "SameGame", |
| "2.0", |
| "SameGame Local Data", |
| 100 |
| ); |
| var dataStr = "INSERT INTO Scores VALUES(?, ?, ?, ?)"; |
| var data = [ |
| gameMode, |
| score, |
| maxColumn + "x" + maxRow, |
| Math.floor(gameDuration / 1000) |
| ]; |
| if (score >= gameCanvas.highScore)//Update UI field |
| gameCanvas.highScore = score; |
| |
| db.transaction( |
| function(tx) { |
| tx.executeSql('CREATE TABLE IF NOT EXISTS Scores(game TEXT, score NUMBER, gridSize TEXT, time NUMBER)'); |
| tx.executeSql(dataStr, data); |
| } |
| ); |
| } |
| |
| function getLevelHistory() |
| { |
| var db = Sql.LocalStorage.openDatabaseSync( |
| "SameGame", |
| "2.0", |
| "SameGame Local Data", |
| 100 |
| ); |
| db.transaction( |
| function(tx) { |
| tx.executeSql('CREATE TABLE IF NOT EXISTS Puzzle(level TEXT, score NUMBER, moves NUMBER, time NUMBER)'); |
| var rs = tx.executeSql('SELECT * FROM Puzzle WHERE level = "' + puzzlePath + '" ORDER BY score desc'); |
| if (rs.rows.length > 0) { |
| gameCanvas.puzzleWon = true; |
| gameCanvas.highScore = rs.rows.item(0).score; |
| } else { |
| gameCanvas.puzzleWon = false; |
| gameCanvas.highScore = 0; |
| } |
| } |
| ); |
| } |
| |
| function saveLevelHistory() |
| { |
| var db = Sql.LocalStorage.openDatabaseSync( |
| "SameGame", |
| "2.0", |
| "SameGame Local Data", |
| 100 |
| ); |
| var dataStr = "INSERT INTO Puzzle VALUES(?, ?, ?, ?)"; |
| var data = [ |
| puzzlePath, |
| gameCanvas.score, |
| gameCanvas.moves, |
| Math.floor(gameDuration / 1000) |
| ]; |
| gameCanvas.puzzleWon = true; |
| |
| db.transaction( |
| function(tx) { |
| tx.executeSql('CREATE TABLE IF NOT EXISTS Puzzle(level TEXT, score NUMBER, moves NUMBER, time NUMBER)'); |
| tx.executeSql(dataStr, data); |
| } |
| ); |
| } |
| |
| function nuke() //For "Debug mode" |
| { |
| for (var row = 1; row <= 5; row++) { |
| for (var col = 0; col < 5; col++) { |
| if (board[index(col, maxRow - row)] != null) { |
| board[index(col, maxRow - row)].dying = true; |
| board[index(col, maxRow - row)] = null; |
| } |
| } |
| } |
| if (gameMode == "multiplayer" && gameCanvas.curTurn == 2) |
| shuffleUp(); |
| else |
| shuffleDown(); |
| if (gameMode == "endless") |
| refill(); |
| else |
| victoryCheck(); |
| } |