Qt Quick Demo - Same Game
/**************************************************************************** ** ** 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(); }