diff --git a/app.js b/app.js index b56d131..da04a2e 100644 --- a/app.js +++ b/app.js @@ -1,23 +1,24 @@ -var createError = require('http-errors'); -var express = require('express'); -var path = require('path'); -var cookieParser = require('cookie-parser'); -var logger = require('morgan'); -var indexRouter = require('./routes/index'); -var usersRouter = require('./routes/users'); +var createError = require('http-errors') +var express = require('express') +var path = require('path') +var cookieParser = require('cookie-parser') +var logger = require('morgan') +var indexRouter = require('./routes/index') +var usersRouter = require('./routes/users') const config = require('./config') const gameEngine = require('./game/gameEngine') +const socketManager = require('./game/socketManager') var app = express(); // view engine setup -app.set('views', path.join(__dirname, 'views')); +app.set('views', path.join(__dirname, 'views')) app.set('view engine', 'jade'); app.use(logger('dev')); app.use(express.json()); -app.use(express.urlencoded({ extended: false })); +app.use(express.urlencoded({ extended: false })) app.use(cookieParser()); -app.use(express.static(path.join(__dirname, 'public'))); +app.use(express.static(path.join(__dirname, 'public'))) app.use('/', indexRouter); app.use('/users', usersRouter); diff --git a/bin/www b/bin/www index d814c1d..f13d8a1 100755 --- a/bin/www +++ b/bin/www @@ -101,7 +101,10 @@ let result = figlet("Burger Kin",(error,result) => { console.log("written by Oren Zakay and Alon Genosar, Kin.org\n\nconfig:") jclrz(config) console.log('\n') - + start() }) -require('../game/socketManager')(server) \ No newline at end of file + const start = async () => { + await require('../core/blockchain').init() + require('../game/socketManager')(server) +} diff --git a/config.js b/config.js index 37a426a..ef6fd78 100644 --- a/config.js +++ b/config.js @@ -14,8 +14,14 @@ module.exports = { master_public_address: process.env.hasOwnProperty('master_pub board_height: process.env.hasOwnProperty('board_height') ? parseInt(process.env.board_height) : 5, monitor_tables: process.env.hasOwnProperty('monitor_tables') ? parseInt(process.env.monitor_tables) : false, monitor_tables_interval: process.env.hasOwnProperty('monitor_tables_interval') ? parseInt(process.env.monitor_tables_interval) : 2000, - game_fee: process.env.hasOwnProperty('game_fee') ? parseFloat(process.env.game_fee) : 10, + game_fee: process.env.hasOwnProperty('game_fee') ? parseFloat(process.env.game_fee) : 5, bad_card_symbol_index: 1, total_bad_card_pairs: 1, - flipped_card_symbol_index: 0 - }; + flipped_card_symbol_index: 0, + transaction_experation_in_sec: 10, + pre_result_timeout:200, + result_timout:2000, + server_version:0.1, + turn_timeout: process.env.hasOwnProperty('turn_timeout') ? parseInt(process.env.turn_timeout) : 10 * 1000, + totalChannels: process.env.hasOwnProperty('totalChannels') ? parseInt(process.env.totalChannels) : 10 + } diff --git a/core/blockchain.js b/core/blockchain.js index c58e05f..02346ff 100644 --- a/core/blockchain.js +++ b/core/blockchain.js @@ -4,27 +4,38 @@ * * Desc * - * @author Oren Zakay. + * @author Oren Zakay, Alon Genosar */ -const KinClient = require('@kinecosystem/kin-sdk-node').KinClient; -const Environment = require('@kinecosystem/kin-sdk-node').Environment; +const { KinClient, Transaction, Environment, Channels } = require('@kinecosystem/kin-sdk-node') const config = require('../config') - const client = new KinClient(Environment.Testnet); - let masterAccount -async function getMasterAccount() { - if (!masterAccount) { - masterAccount = await client.createKinAccount({ - seed: config.master_seed, - appId: config.appId - }); - } - return masterAccount +async function init() { + console.log("Creating channels") + let keepers = await Channels.createChannels({ + environment: Environment.Testnet, + baseSeed: config.master_seed, + salt: "Dubon Haya Po", + channelsCount: config.totalChannels, + startingBalance: 0 + }) + + let keys = keepers.map( item => { + return item.seed + }) + console.log("Creating master account") + masterAccount = await client.createKinAccount({ + seed: config.master_seed, + appId: config.appId, + channelSecretKeys:keys + }); + console.log("Channels created succsesfully") } + + async function isAccountExisting(wallet_address) { try { const result = await client.isAccountExisting(wallet_address) @@ -34,26 +45,30 @@ async function isAccountExisting(wallet_address) { return false } } +//console.log(Environment.Testnet.passphrase) async function validateTransaction(transactionId) { - const data = await client.getTransactionData(transactionId) - - return data - //check for correct amount - && data.hasOwnProperty('amount') - && data.amount === config.game_fee - //check for transaction date - && data.hasOwnProperty('timeStamp') - && new Date() - Date(data.timestamp) < 10 + try { + const data = await client.getTransactionData(transactionId) + return data + //check for correct amount + && data.hasOwnProperty('amount') + && data.amount === config.game_fee + //check for transaction date + && data.hasOwnProperty('timeStamp') + && new Date() - Date(data.timestamp) < config.transaction_experation_in_sec // 10 sec + } + catch { + return false + } } + async function createAccount(wallet_address) { console.log("buildCreateAccount -> " + wallet_address) - // Sign the account creation transaction - const masterAccount = await getMasterAccount() let createAccountBuilder = await masterAccount.buildCreateAccount({ address: wallet_address, startingBalance: 100, - fee: 100, + fee: 0, memoText: "C" + createID(9) }) @@ -63,18 +78,25 @@ async function createAccount(wallet_address) { console.log("createAccount transaction id -> ", id) } +async function whitelistTransaction(walletPayload) { + try { + const whitelistTx = await masterAccount.whitelistTransaction(walletPayload) + return whitelistTx + } catch(error) { + throw error + } +} async function payToUser(wallet_address, amount) { - //console.log("payToUser -> " + wallet_address + " with amount = " + amount) - const masterAccount = await getMasterAccount() - const transactionBuilder = await masterAccount.buildSendKin({ - address: wallet_address, - amount: amount, - fee: 100, - memoText: createID(10) + masterAccount.channelsPool.acquireChannel( async channel => { + const transactionBuilder = await masterAccount.buildSendKin({ + address: wallet_address, + amount: amount, + fee: 0, + memoText: createID(10), + channel: channel + }) + return await masterAccount.submitTransaction(transactionBuilder) }) - - await masterAccount.submitTransaction(transactionBuilder) - console.log("payToUser submitTransaction -> ", transactionBuilder) } function createID(length) { @@ -91,5 +113,7 @@ module.exports = { validateTransaction, isAccountExisting, createAccount, - payToUser + payToUser, + whitelistTransaction, + init } \ No newline at end of file diff --git a/game/Game.js b/game/Game.js index cd5182a..8aa7c8a 100644 --- a/game/Game.js +++ b/game/Game.js @@ -15,17 +15,18 @@ for(var i = 0; i < config.board_width * config.board_height / 2.0; i++) { symbols.push(i + 1) symbols.push(i + 1) } -const states = Object.freeze({ PENDING: 'pending', PLAYING: 'playing', COMPLETED: 'completed' }) +const states = Object.freeze({ PENDING: 'pending', STARTING:'starting', TURN: 'turn', RESULT: 'result', COMPLETED: 'completed' }) class Game { static get states() { return states } constructor() { this.id = newId(5) - this.state = 'pending' + this.state = states.PENDING this.players = {} this.flipped = [] this.turn = null + this.stateValue = null this.boardSize = [config.board_width,config.board_height] this.board = this.shuffle(JSON.parse(JSON.stringify(symbols))) } @@ -46,12 +47,11 @@ class Game { let cpy = JSON.parse(JSON.stringify(this)) cpy.board = cpy.board.map( item => { return item == null ? null : Math.min(item,0) } ) this.flipped.forEach( index => { cpy.board[index] = this.board[index] }); - delete cpy.flipped return cpy } cardsLeft() { - return this.board.filter( item => { return item != null }).length + return this.board.filter( item => { return item != null }) } } diff --git a/game/Player.js b/game/Player.js index 8b73605..1d84ef1 100644 --- a/game/Player.js +++ b/game/Player.js @@ -9,10 +9,12 @@ class Player { - constructor({id,name}) { + constructor({id,name,facebookId,avatar}) { this.id = id this.name = name this.score = 0 + this.facebookId = facebookId + this.avatar = avatar } } module.exports = Player \ No newline at end of file diff --git a/game/gameEngine.js b/game/gameEngine.js index e71d657..51e4ff5 100644 --- a/game/gameEngine.js +++ b/game/gameEngine.js @@ -13,9 +13,9 @@ const Game = require('./Game') const Player = require('./Player') const blockchain = require('../core/blockchain') const events = require('events') - const Spinner = require('cli-spinner').Spinner; const spinner = new Spinner("Monitoring Games") +const timerByGameId = {} spinner.setSpinnerString(2) //ENUMS @@ -26,18 +26,22 @@ var games = [] var gamesByUserId = {} //Utils -function gameEmit( {gameId, action, sender = "server", callerId, value } ) { - module.exports.eventEmitter.emit('action', { action:action, gameId:gameId, callerId:callerId, value:value }) +function gameEmit( {gameId, action, sender = "server", callerId, value, result } ) { + module.exports.eventEmitter.emit('action', { action:action, gameId:gameId, callerId:callerId, value:value, result:result }) } //API module.exports = { eventEmitter: new events.EventEmitter() ,actions: actions - ,isInGame(callerId) { - return (gamesByUserId[callerId]) + ,isPlayerInGame(callerId) { + return gamesByUserId[callerId] !== undefined } ,reset: () => { + timerByGameId.forEach( timer => { + clearTimeout( timer ) + }) + timerByGameId = {} games = [] gamesByUserId = {} } @@ -52,14 +56,13 @@ module.exports = { }, interval); } ,doAction: async ({action,callerId,value}) => { - if(!config.monitor_tables) - console.log("[gameEngine] doAction",{action:action,callerId:callerId,value:value}) + // if(!config.monitor_tables) + // console.log("[gameEngine] doAction",{action:action,callerId:callerId,value:value}) if( !callerId ) throw new Error("Missing callerId") var game = gamesByUserId[callerId] switch (action) { - // // Recover // @@ -71,45 +74,71 @@ module.exports = { // // Join // - case actions.JOIN: - const result = await blockchain.isAccountExisting(callerId) - if(!result) throw new Error("Invalid public id") - - game = game || games.filter( game => game.state == Game.states.PENDING )[0] || new Game() + case actions.JOIN: + if(game) + return game.userFriendly() + + //Validate + if( !value ) throw new Error("Missing transaction id" ) + if( !value.name ) throw new Error("Missing name" ) + //if( !value.transactionId ) throw new Error("Missing transaction id" ) + if(!await blockchain.isAccountExisting(callerId) ) throw new Error("Invalid public id") + + //Validate transaction + await !blockchain.validateTransaction(value.transactionId) + + //Check for pending games + game = games.filter( game => game.state == Game.states.PENDING && Object.keys(game.players).length < 2 )[0] || new Game() + + //Push game to games list if not already there if(games.indexOf(game) < 0 ) games.push(game) - // If new game created , check player's transaction - if( game.players.length == 0 ) { - if( !value ) throw new Error("Missing transaction id") - if( await !blockchain.validateTransaction(value)) - throw new Error("Invalid transaction Id") - } - game.players[callerId] = new Player( { id:callerId, name:value } ) + //Add player to game object + game.players[callerId] = new Player( { id:callerId, name:value.name,facebookId: value.facebookId,avatar: value.avatar } ) + + //Index games by player gamesByUserId[callerId] = game + + //Change state to starting if two player's has joind + if( Object.keys(game.players).length == 2 ) + game.state = Game.states.STARTING - game = game.userFriendly() - if( game.state == Game.states.PENDING && Object.keys(game.players).length == 2 ) { + //Start game + if( game.state == Game.states.STARTING ) { setTimeout( async function() { - let result = await module.exports.doAction({ action:actions.TURN, callerId:callerId } ) - gameEmit( { gameId:game.id, action:actions.TURN, value:result, callerId:callerId } ) + module.exports.doAction({ action:actions.TURN, callerId:callerId } ) }, 1000); } - return game + return game.userFriendly() // // Turn // case actions.TURN: if(!game) throw new Error("User not in game") - if(game.state != Game.states.PENDING && game.state != Game.states.PLAYING ) throw new Error("Turn not allowed") - + if(game.state != Game.states.STARTING && game.state != Game.states.TURN && game.state != Game.states.RESULT ) throw new Error("Turn not allowed") + + //Clear turn timeout + clearTimeout( timerByGameId[game.id]) + delete game.stateTimeout + const playersId = Object.keys(game.players) const i = playersId.indexOf(game.turn) - game.turn = playersId[ (i + 1) % playersId.length] - game.state = Game.states.PLAYING + game.turn = value || playersId[ (i + 1) % playersId.length] + game.state = Game.states.TURN game.flipped = [] - return game.turn + + //Set turn timout  + game.stateTimeout = new Date().getTime() + config.turn_timeout + game.stateTotalTimeout = config.turn_timeout + + timerByGameId[game.id] = setTimeout( async () => { + delete timerByGameId[game.id] + module.exports.doAction({ action:actions.RESULT, callerId:callerId } ) + },config.turn_timeout ) + gameEmit({ gameId:game.id,action:actions.TURN, value:value,result: game.userFriendly() } ) + return game.userFriendly() // // Flip @@ -117,75 +146,89 @@ module.exports = { case actions.FLIP: value = parseInt(value) if( !game ) throw new Error("User not in game") - if( game.state != Game.states.PLAYING ) throw new Error("Turn not allowed in that state") + if( game.state != Game.states.TURN ) throw new Error("Turn not allowed in that state") if( game.turn != callerId) throw new Error("Not your turn") - if( value === undefined || value !== parseInt(value)) throw new Error("Invalid value") + if( value === undefined) throw new Error("Invalid value") if( game.flipped && game.flipped.indexOf(value) > -1 ) throw new Error("Card already flipeed") if( game.flipped && game.flipped.length == 2 ) throw new Error("Cards already flipped") if( game.board[value] === null ) throw new Error("Card alread removed") game.flipped = game.flipped || [] game.flipped.push(value) - + + if( game.flipped.length == 2 ) { + clearTimeout( timerByGameId[game.id] ) setTimeout( async function() { - let result = await module.exports.doAction({action:actions.RESULT,callerId:callerId}) - gameEmit( { gameId:game.id,action:actions.RESULT, value:result,callerId:callerId } ) - }, 1000); - } - return { position:value, symbol:game.board[value]} + module.exports.doAction({ action: actions.RESULT, callerId }) + }, 1500 ); + } + return game.userFriendly() // // Result // case actions.RESULT: - const match = game.flipped.length == 2 && game.board[game.flipped[0]] === game.board[game.flipped[1] ] - var p = null - if(match) { - const cardValue = game.board[game.flipped[0]] - game.flipped.forEach( i => { game.board[i] = null }) - p = game.players[callerId] - p.score = cardValue != config.bad_card_symbol_index ? p.score + 1 : -1 + let match = false + if(game.flipped.length < 2) { + game.players[callerId].turnMissed = game.players[callerId].turnMissed || 0 + 1 + } else { + match = game.flipped.length == 2 && game.board[game.flipped[0]] === game.board[game.flipped[1] ] + var p = null + if(match) { + const cardValue = game.board[game.flipped[0]] + game.flipped.forEach( i => { game.board[i] = null }) + p = game.players[callerId] + p.score += cardValue != config.bad_card_symbol_index ? 1 : -1 + } } - - setTimeout( async function() { - if( game.cardsLeft() > 1 && game.players[callerId].score > -1 ) { - let result = await module.exports.doAction({action:actions.TURN,callerId:callerId}) - gameEmit( { gameId:game.id,action:actions.TURN, value:result } ) + game.state = Game.states.RESULT + gameEmit( { gameId:game.id,action:actions.RESULT, value: match, callerId: "server",result: game.userFriendly()} ) + + var cardsLeft = game.cardsLeft() + + + + if( cardsLeft.length > 2 || ( cardsLeft[0] && cardsLeft[0] != config.bad_card_symbol_index && cardsLeft[0] )) { + module.exports.doAction({ action: actions.TURN, callerId: callerId, value: match ? callerId : undefined }) + } + else { + var winnerId = "tie" + let players = Object.values(game.players) + if( players[0].score !== players[1].score ) + winnerId = players[0].score > players[1].score ? players[0].id : players[1].id + players.forEach( player => { delete gamesByUserId[player.id] }) + games.splice(games.indexOf(game),1) + if(winnerId == 'tie') { + blockchain.payToUser( players[0].id, config.game_fee) + blockchain.payToUser( players[1].id, config.game_fee) } else { - let result = await module.exports.doAction({action:actions.WIN,callerId:callerId}) - gameEmit( { gameId:game.id,action:actions.WIN, value:result } ) + blockchain.payToUser(winnerId, config.game_fee * 2) } - }, 3000); - return { match:match, callerId:callerId, positions:game.flipped,player:p} - - // - // Win - // - case actions.WIN: - let players = Object.values(game.players) - var winnerId = players[0].score > players[1].score ? players[0].id : players[1].id - players.forEach( player => { delete gamesByUserId[player.id] }) - games.splice(games.indexOf(game),1) - blockchain.payToUser(winnerId,config.game_fee) - return winnerId + game.state = Game.states.COMPLETED + gameEmit( { gameId:game.id,action:actions.WIN, value:winnerId, result: game } ) + } + // }, 100); break - + // // Leave // case actions.LEAVE: if( game && game.state == Game.states.PENDING ) { + delete game.players[callerId] delete gamesByUserId[callerId] if(!Object.keys(game.players).length) games.splice(games.indexOf(game),1) + + blockchain.payToUser(callerId, config.game_fee ) } break default: - throw new Error("Action",action," not supported") + throw new Error("Action",action,"not supported") } } } \ No newline at end of file diff --git a/game/socketManager.js b/game/socketManager.js index cf8aa8a..36dade1 100644 --- a/game/socketManager.js +++ b/game/socketManager.js @@ -7,6 +7,7 @@ * @author Alon Genosar. */ + let config = require('../config') const { doAction, actions, eventEmitter,test } = require('./gameEngine') const gameEngine = require('./gameEngine') @@ -16,45 +17,60 @@ const socketByUserId = [] const allowedUserActions = [ actions.FLIP, actions.JOIN, actions.RECOVER ] // Game engine event listener -eventEmitter.on("action",( {gameId,action,callerId,value} ) => { - io.to(gameId).emit("action", { action:action, callerId:callerId, value:value }) +eventEmitter.on("action",( {gameId,action,callerId,value, result} ) => { + io.to(gameId).emit("action", { action:action, callerId:callerId, value:value,result:result }) }) - // API module.exports = function (server,options,cb) { + console.log("Starting socket manager") io = require('socket.io')(server) - io.on('connection', async function (socket,next) { - //console.log("Connecting",socket.handshake.query.token, socket.handshake.query.name) + io.on('connection', async function (socket,next,a) { if (socket.handshake.query && socket.handshake.query.token && socket.handshake.query.token != 'undefined' && socket.handshake.query.name && socket.handshake.query.name != 'undefined') { try { - //console.log(socket.handshake.query.token ,socket.handshake.query.name) - let game = await doAction({ action:actions.JOIN, callerId: socket.handshake.query.token ,value:socket.handshake.query.name,socket:socket }) + let value = { + name: socket.handshake.query.name, + transactionId: socket.handshake.query.transactionId, + facebookId: socket.handshake.query.facebookId, + avatar: socket.handshake.query.avatar + } + + let game = await doAction({ action:actions.JOIN, callerId:socket.handshake.query.token, value, socket }) socket.gameId = game.id socket.join(game.id) socket.token = socket.handshake.query.token - io.to(game.id).emit("action",{action:actions.JOIN, callerId:socket.token,value:game}) + io.to(game.id).emit("action",{action:actions.JOIN, callerId:socket.token,value,result:game}) } catch(error) { - console.log("error",error) socket.disconnect() } } else { socket.disconnect() } socket.on('action',async function (action,value,cb) { - // console.log("socket recieved aciton",action,value, socket.isAuthorized) if( socket.token && allowedUserActions.indexOf(action) > -1 ) { try { - let result = await doAction({action:action,callerId:socket['token'], value:value,socket:socket}) + let result = await doAction({action, callerId:socket['token'], value, socket}) + if(cb) cb(result) - if(socket.gameId) - io.to(socket.gameId).emit("action",{action:action,callerId:socket.token ,value:{result}}) + + if( result && result.hasOwnProperty('id') && !socket.gameId ) { + socket.gameId = result.id + socket.join(result.id) + socket.token = socket.handshake.query.token + } + + if(socket.gameId) + io.to(socket.gameId).emit("action", { action: action, + callerId:socket.token, + value: value, + result: result + }) } catch(error) { if(cb) diff --git a/public/socketio.html b/public/socketio.html index 9829897..cc841be 100644 --- a/public/socketio.html +++ b/public/socketio.html @@ -1,7 +1,9 @@