oglimmer/citybuilder

View on GitHub
server/rule_game.js

Summary

Maintainability
D
1 day
Test Coverage
var Player = require("./rule_player.js");
var CardStack = require("./rule_cardstack.js");
var GameField = require('./rule_gamefield.js');
var Field = require('./rule_field.js');
var CardFactory = require('./rule_cardfactory.js');
var MonthHelper = require('./rule_dateutil.js');
var FieldType = require("./rule_defines.js").FieldType;
var log4js = require('log4js');
var logger = log4js.getLogger('game');
var AllCards = require('./rule_card.js');
var HouseTypeMaxPop = require("./rule_defines.js").HouseTypeMaxPop;
var HouseTypeReverse = require("./rule_defines.js").HouseTypeReverse;
var GameStates = require("./rule_defines.js").GameStates;

function Game() {
    this.clazz = "Game";
    this.playerIds = {}; // k=player.number, v=player.id
    this.playerNames = [];
    
    this.biddings = {};
    this.auctionTakeOrder = [];
    this.cardsToAuction = [];

    this.maxPlayerNumber = 0;

    /* 0 = game created, but waiting for players
     * 1 = deploy card
     * 2 = set bid
     * 3 = select card for auction
     * 4 = initial card selection
     */
    this.gameState = GameStates.WAITING_FOR_PLAYERS;

    this.currentDate = new MonthHelper();
    this.gameField = new GameField(this);
    this.cardsCommercial = new CardStack();
    this.cardsMisc = new CardStack();
    this.initTurn();
}

Game.prototype.startGame = function(playTime, allPlayers) {
    this.playTime = playTime;
    this.gameState = GameStates.PICK_INITIAL_CARD;
    this.cardsCommercial.create(0, 1);
    allPlayers.forEach(function(player) {
        if(this.cardsCommercial.length() < 4) {
            this.cardsCommercial.clear();
            this.cardsCommercial.create(0, 1);
        }
        var cardsToSelect = [];
        for(var i = 0 ; i < 4 ; i++) {
            cardsToSelect.push(this.cardsCommercial.removeTop());
        }
        this.cardsToAuction.push({playerId: player._id, cardsToSelect: cardsToSelect});
    }.bind(this));
    this.cardsCommercial.clear();
    this.cardsCommercial.create(0, CardStack.NUMBER_PER_CARD);
    this.cardsMisc.create(1, CardStack.NUMBER_PER_CARD);
}

Game.prototype.createPlayer = function(socketId, playerName, onCreated) {
    var newPlayer = new Player(this, socketId, playerName);
    var PlayerManager = require('./rule_playermanager.js');
    var GameManager = require('./rule_gamemanager.js');
    PlayerManager.storePlayer(newPlayer, null, function(savedPlayer) {
        // update Game object with added player (and increased player no)
        var nextPlayerNo;
        GameManager.storeGame(this, function(gameToPrepare) {
            nextPlayerNo = gameToPrepare.maxPlayerNumber++;
            gameToPrepare.playerIds[nextPlayerNo] = savedPlayer._id;            
            gameToPrepare.playerNames.push(playerName);
            gameToPrepare.playersOnTurn.push(savedPlayer._id);
        }, function() {
            // on success save the player again
            PlayerManager.storePlayer(savedPlayer, function(latestPlayer) {
                latestPlayer.no = nextPlayerNo;
            }, function(finalSavedPlayer) {
                onCreated(finalSavedPlayer);
            });            
        });
    }.bind(this));
};

Game.prototype.rejoinPlayer = function(socketId, player, onSuccess) {
    var PlayerManager = require('./rule_playermanager.js');
    var GameManager = require('./rule_gamemanager.js');
    PlayerManager.storePlayer(player, function(player) {
        player.socketId = socketId;    
    }, function(savedPlayer) {
        GameManager.storeGame(this, function(gameToPrepare) {
            //gameToPrepare.playerIds[player.no] = player._id;            
            //gameToPrepare.playerNames.push(player.playerName);
            //gameToPrepare.playersOnTurn.push(player._id);
        }, function(savedGame) {
            onSuccess(savedPlayer);
        });
    }.bind(this));
};

Game.removePlayer = function(socketId, onSuccess, onGameDeleted) {
    var PlayerManager = require('./rule_playermanager.js');
    var GameManager = require('./rule_gamemanager.js');
    PlayerManager.getPlayerBySocketId(socketId, function(player) {
        GameManager.getGame(player.gameId, function(game) {
            if(game.gameState == GameStates.WAITING_FOR_PLAYERS && (game.playerNames.length == 1 || game.playerNames[0] == player.playerName)) {
                logger.debug("[removePlayer] removed last/first player during WAITING_FOR_PLAYERS...deleting game. %s", game._id);
                PlayerManager.getPlayers(game._id, null, function(allPlayers) {
                    onGameDeleted(allPlayers);
                    GameManager.deleteGame(game);
                });                
            } else {
                PlayerManager.storePlayer(player, function(player) {
                    player.socketId = null;    
                    GameManager.storeGame(game, function(gameToPrepare) {
                        if(gameToPrepare.gameState == GameStates.WAITING_FOR_PLAYERS) {
                            delete gameToPrepare.playerIds[player.no];
                            gameToPrepare.playerNames.removeByObj(player.playerName);
                            gameToPrepare.playersOnTurn.removeByObj(player._id);
                        }
                    }, function(savedGame) {
                        onSuccess(savedGame);
                    });
                });
            }
        });
    });
};

Game.prototype.initTurn = function() {
    this.playersOnTurn = [];
    for(var k in this.playerIds) {
        var v = this.playerIds[k];
        this.playersOnTurn.push(v);
    }
}

Game.prototype.setPlayerDone = function(player) {
    for(var i = 0 ; i < this.playersOnTurn.length ; i++) {
        if(this.playersOnTurn[i] == player._id) {
            this.playersOnTurn.splice(i,1);
            break;
        }
    }
}

Game.prototype.isPlayerDone = function(player) {
    for(var i = 0 ; i < this.playersOnTurn.length ; i++) {
        if(this.playersOnTurn[i] == player._id) {            
            return false;
        }
    }
    return true;
}

Game.prototype.allPlayersDone = function(callback) {
    var PlayerManager = require("./rule_playermanager.js");
    PlayerManager.getPlayers(this._id, null, function(allPlayers) {
        logger.debug("[allPlayersDone] allPlayers.length="+allPlayers.length);
        var atLeastOnePlayerInTurn = false;
        this.playersOnTurn.forEach(function(playerIdOnTurn) {
            var player = allPlayers.getPlayerById(playerIdOnTurn);    
            if(player.socketId !== null) {
                atLeastOnePlayerInTurn = true;
            }
        }.bind(this));
        logger.debug("[allPlayersDone] atLeastOnePlayerInTurn="+atLeastOnePlayerInTurn);
        callback(!atLeastOnePlayerInTurn);
    }.bind(this));
}

Game.prototype.allPlayersOffline = function(callback) {
    var PlayerManager = require("./rule_playermanager.js");
    PlayerManager.getPlayers(this._id, null, function(allPlayers) {
        var atLeastOnePlayerOnline = false;
        allPlayers.forEach(function(player) {
            if(player.socketId !== null) {
                atLeastOnePlayerOnline = true;
            }
        }.bind(this));
        logger.debug("[allPlayersOffline] atLeastOnePlayerOnline="+atLeastOnePlayerOnline);
        callback(!atLeastOnePlayerOnline);
    }.bind(this));    
}

Game.prototype.atLeastOneOnline = function(playerIds, callback) {
    var PlayerManager = require("./rule_playermanager.js");
    PlayerManager.getPlayers(this._id, null, function(allPlayers) {
        var atLeastOnePlayerOnline = false;
        allPlayers.forEach(function(player) {
            if(player.socketId !== null && playerIds.in(player._id)) {
                atLeastOnePlayerOnline = true;
            }
        }.bind(this));
        logger.debug("[atLeastOneOnline] atLeastOnePlayerOnline="+atLeastOnePlayerOnline);
        callback(atLeastOnePlayerOnline);
    }.bind(this));    
}

Game.prototype.checkForNextTurn = function() {
    logger.debug("[checkForNextTurn] gameState="+this.gameState);
    this.allPlayersDone(function(allDone) {
        if(allDone) {
            var PlayerManager = require("./rule_playermanager.js");
            PlayerManager.getPlayers(this._id, null, function(allPlayers) {            
                if(this.gameState == GameStates.CITY_VIEW) {
                    this.processNextTurn(allPlayers);
                } else if(this.gameState == GameStates.SET_BIDDING) {
                    this.processAuctionBid(allPlayers);
                } else if(this.gameState == GameStates.PICK_CARD) {
                    if(this.auctionTakeOrder.length > 0) {
                        // no all players picked their card yet
                        this.processPostAuctionSelection();
                    } else {
                        // all cards picked, go to next turn
                        this.processAuctionSelect(allPlayers);
                    }
                } else if(this.gameState == GameStates.PICK_INITIAL_CARD) {
                    this.initialCardSelectionDone(allPlayers);
                }
            }.bind(this));
        }
    }.bind(this));
}

Game.prototype.initialCardSelectionDone = function(allPlayers) {
    logger.debug("[initialCardSelectionDone] start");

    this.gameState = GameStates.CITY_VIEW;
    this.initTurn();
    var GameManager = require("./rule_gamemanager.js");
    GameManager.storeGame(this, null);
    allPlayers.forEach(function(player) {
        var psoc = require('./socket.js')(player);
        psoc.sendInitUIElements(this);
    }.bind(this));
}

Game.prototype.processAuctionSelect = function(allPlayers) {
    logger.debug("[processAuctionSelect] start");

    this.gameState = GameStates.CITY_VIEW;
    this.initTurn();
    var lastBiddings = [];
    allPlayers.forEach(function(player) {
        var bid = this.biddings[player._id];
        if(typeof(bid) !== 'undefined') {
            lastBiddings.push({playerName : player.playerName, bid : bid.originalBid, cost : bid.acutalCost});
        } else {
            lastBiddings.push({playerName : player.playerName, bid:0,cost:0});
        }
    }.bind(this));
    this.biddings = {};
    var GameManager = require("./rule_gamemanager.js");
    GameManager.storeGame(this, null);
    allPlayers.forEach(function(player) {
        var psoc = require('./socket.js')(player);    
        psoc.sentshowFieldPane({ gameState : this.gameState, lastBids : lastBiddings });
    }.bind(this));
}

Game.prototype.payBid = function(playerIds, allPlayers, part) {
    var PlayerManager = require("./rule_playermanager.js");
    playerIds.forEach(function(pId) {
        var player = allPlayers.getPlayerById(pId);
        var bid = this.biddings[pId];
        var cost = bid.originalBid * part;
        bid.acutalCost = cost;
        logger.debug("[payBid] Player "+player.playerName+" bid $"+bid.originalBid+" by "+part+"= $"+cost);
        player.money -= cost;
        PlayerManager.storePlayer(player, null);        
    }.bind(this));
    allPlayers.forEach(function(player) {
        if(player.money < 0) {
            // pay interest for debts 
            var interest= player.money*0.03;
            logger.debug("[payBid] Player "+player.playerName+" paid $"+interest+" interest");
            player.money -= interest;
        }
    });
}

Game.prototype.processAuctionBid = function(allPlayers) {
    logger.debug("[processAuctionBid] start. this.biddings=%j", this.biddings);

    this.gameState = GameStates.PICK_CARD;
    // if a player had left the game and rejoined again his bidding is undefined. So tread this as $0.
    allPlayers.forEach(function(player) {
        if(player.socketId !== null && typeof(this.biddings[player._id]) === 'undefined') {
            this.biddings[player._id] = { originalBid: 0, acutalCost: 0 };
            logger.debug("[processAuctionBid] added 0-bidding:%s", player.playerName);
        }
    }.bind(this));

    // order by bid and create turn order
    this.auctionTakeOrder = [];
    var tmpBiddings = JSON.parse(JSON.stringify(this.biddings)); // deep-copy
    var tmpAllPlayers = allPlayers.asIdArray();
    logger.debug("[processAuctionBid] tmpAllPlayers:%j", tmpAllPlayers);
    while(Object.keys(tmpBiddings).length !== 0) {

        logger.debug("[processAuctionBid] run:"+Object.keys(tmpBiddings).length);

        var maxBid = -1;
        var maxBidders;
        for(var playerId in tmpBiddings) {
            var originalBid = parseInt(tmpBiddings[playerId].originalBid);
            if(maxBid < originalBid) {
                maxBid = originalBid;
                maxBidders = [playerId];                
            } else if(maxBid == originalBid) {
                maxBidders.push(playerId);
            }
        }

        logger.debug("[processAuctionBid] maxBid:%d, maxBidders:%j", maxBid, maxBidders);

        maxBidders.forEach(function(oneMaxBidder) {
            delete tmpBiddings[oneMaxBidder];
            tmpAllPlayers.removeByObj(oneMaxBidder);
        });
        this.auctionTakeOrder.push(maxBidders);
    }

    // calc final bid cost and subtract money
    this.payBid(this.auctionTakeOrder[0], allPlayers, 1);
    for(var i = 1 ; i < this.auctionTakeOrder.length-1 ; i++) {
        var max = this.auctionTakeOrder.length-1;
        var part = (1/max)*(max-i);
        this.payBid(this.auctionTakeOrder[i], allPlayers, part);        
    }

    // if there are players who haven't bid anything, add them as a last round
    if(tmpAllPlayers.length > 0) {
        this.auctionTakeOrder.push(tmpAllPlayers);
        logger.debug("[processAuctionBid] added final round for non-bidders: %j", tmpAllPlayers);
    }

    this.doNextAuctionPickCard(allPlayers);
}

Game.prototype.processPostAuctionSelection = function() {
    logger.debug("[processPostAuctionSelection] start");
    var PlayerManager = require("./rule_playermanager.js");
    PlayerManager.getPlayers(this._id, null, function(allPlayers) {            
        this.doNextAuctionPickCard(allPlayers);
    }.bind(this));
}

Game.prototype.doNextAuctionPickCard = function(allPlayers) {
    logger.debug("[doNextAuctionPickCard] start. auctionTakeOrder=%d", this.auctionTakeOrder.length);
    if(this.auctionTakeOrder.length == 0 ) {
        this.checkForNextTurn();
    } else {
        /* readies the next player(s) for the given bid
         * => puts them into playersOnTurn, and send message over */
        var    nextPlayerIds = this.auctionTakeOrder.shift();
        logger.debug("[doNextAuctionPickCard] nextPlayerIds=%j", nextPlayerIds);
        this.atLeastOneOnline(nextPlayerIds, function(atLeastOnePlayerOnline) {
            if(!atLeastOnePlayerOnline) {
                this.doNextAuctionPickCard(allPlayers);
            } else {
                this.playersOnTurn = JSON.parse(JSON.stringify(nextPlayerIds)); // deep-copy
                
                var GameManager = require("./rule_gamemanager.js");
                GameManager.storeGame(this, null);

                nextPlayerIds.forEach(function(playerId) {
                    var player = allPlayers.getPlayerById(playerId);
                    var psoc = require('./socket.js')(player);    
                    psoc.sendPostAuctionSelection({ gameState : this.gameState, money: player.money });                    
                }.bind(this));
            }
        }.bind(this));
    }
}

Game.prototype.removeCardFromAuction = function(cardId, playerId) {
    var c = null;
    if(this.gameState == GameStates.PICK_CARD) {
        for(var i = 0 ; i < this.cardsToAuction.length ; i++) {
            if(this.cardsToAuction[i].id == cardId) {
                c = this.cardsToAuction[i];
                this.cardsToAuction.splice(i,1);
            }
        }
    } else {
        for(var j = 0 ; j < this.cardsToAuction.length ; j++) {
            if( this.cardsToAuction[j].playerId == playerId ) {
                for(var i = 0 ; i < this.cardsToAuction[j].cardsToSelect.length ; i++) {
                    if(this.cardsToAuction[j].cardsToSelect[i].id == cardId) {
                        c = this.cardsToAuction[j].cardsToSelect[i];
                    }
                }        
            }
        }
    }
    logger.debug("[removeCardFromAuction] requested:%d, got:%d",cardId,(c!=null?c.id:"-1"));
    return c;
}

Game.prototype.processNextTurn = function(allPlayers) {
    logger.debug("[processNextTurn] start");

    this.gameState = GameStates.SET_BIDDING;

    var changedFields = this.growCity();

    this.resetSuppliesAndCalcBuildstate(changedFields);

    this.calcLocalLevel(changedFields); // no dependency, set:localLevel

    this.doSocialChanges(); // depends on localLevel, set:attachedCard.houseType

    this.calcSupplies(); // depends on profit(attachedCard.houseType), set:supply

    var incomeReceipt = this.calcIncome(allPlayers); // depends on supply and profit(attachedCard.houseType)

    this.currentDate.nextMonth();
    this.playTime--;
    this.initTurn();
    if(this.playTime == 0) {
        this.processEndGame(allPlayers);
    } else {
        var cardsToAuction = [];
        for(var i = 0 ; i < allPlayers.length ; i++) {
            cardsToAuction.push(this.cardsCommercial.removeTop());
        }
        cardsToAuction.push(this.cardsMisc.removeTop());
        this.sendAuction(allPlayers, changedFields, cardsToAuction, incomeReceipt);
    }    
}

Game.prototype.growCity = function() {
    var houseType = Math.floor((Math.random()*3))*3+3; // random = {3,6,9}
    return this.gameField.add(3,3,houseType, HouseTypeMaxPop[HouseTypeReverse[houseType]], houseType==3?30:houseType==6?0:-10);
}

Game.prototype.sendAuction = function(allPlayers, changedFields, cardsToAuction, incomeReceipt) {
    this.cardsToAuction = cardsToAuction;
    allPlayers.forEach(function(player) {
        this.sendAuctionToPlayer(player, changedFields, incomeReceipt);
    }.bind(this));
    var GameManager = require("./rule_gamemanager.js");
    GameManager.storeGame(this, null);        
}

Game.prototype.sendAuctionToPlayer = function(player, changedFields, incomeReceipt) {
    player.availableActions |= 1; // a card is deployable
    var newTurnData = this.constructNewTurnData(player, changedFields, incomeReceipt);    
    var PlayerManager = require("./rule_playermanager.js");
    PlayerManager.storePlayer(player,null, function(savedPlayer) {
        require('./socket.js')(savedPlayer).startAuction(newTurnData);
    });    
}

Game.prototype.resetSuppliesAndCalcBuildstate = function(changedFields) {
    for(var k in this.gameField.fields) {
        var field = this.gameField.fields[k];            
        if(field.buildState == 1) {
            field.buildState++;
            changedFields[field.x+":"+field.y] = field;
        }            
        if(field.type === FieldType.HOUSE) {
            field.attachedCard.supply = [];
            field.localLevel = field.baseLocalLevel;                
        } else {
            //CRAZINESS: add methods to this card
            AllCards.Card.reinit(field.attachedCard);
        }
    }
}

Game.prototype.calcSupplies = function() {
    for(var k in this.gameField.fields) {
        var field = this.gameField.fields[k];            
        if(field.type !== FieldType.HOUSE) {            
            field.attachedCard.calcSupplies(field, this.gameField.fields);
        }
    }
}

Game.prototype.calcLocalLevel = function(changedFields) {
    for(var k in this.gameField.fields) {
        var field = this.gameField.fields[k];            
        if(field.type !== FieldType.HOUSE) {
            field.attachedCard.calcLocalLevel(field, this.gameField.fields, changedFields);
        }
    }
}

Game.prototype.doSocialChanges = function() {
    for(var k in this.gameField.fields) {
        var field = this.gameField.fields[k];            
        if(field.type === FieldType.HOUSE) {
            Field.socialChange(field);
        }
    }        
}

Game.prototype.calcIncome = function(allPlayers) {
    var incomeReceipt = [];
    for(var k in this.gameField.fields) {
        var field = this.gameField.fields[k];
        if(field.type !== FieldType.HOUSE) {
            var player = allPlayers.getPlayerByNo(field.owner);
            var fieldIncome = field.attachedCard.calcRent(field, this.gameField.fields);
            player.money += fieldIncome;
            incomeReceipt.push([player.playerName,fieldIncome,field.attachedCard.title,field.x,field.y]);
        }
    }
    return incomeReceipt;
}

Game.prototype.processEndGame = function(allPlayers) {
    var winner = { player: null, maxMoney : -1, scoreList : [] };
    allPlayers.forEach(function(player) {
        if(player.money > winner.maxMoney) {
            winner.playerName = player.playerName;
            winner.maxMoney = player.money;
        }
        winner.scoreList.push({playerName : player.playerName, money: player.money});
    });            
    allPlayers.forEach(function(player) {
        require('./socket.js')(player).sendEndGame({ winner: winner});                
    });
    var GameManager = require("./rule_gamemanager.js");
    GameManager.deleteGame(this);
}

Game.prototype.constructMsgInfoBar = function(player) {
    return {
        gameState : this.gameState,
        money : player.money,
        playerNumber : this.playerNames.length,
        currentDate : this.currentDate.toString() +" ("+this.playTime+")"
    };
}

Game.prototype.constructNewTurnData = function(player, changedFields, incomeReceipt) {
    return {
        gameState : this.gameState,
        infoBar : this.constructMsgInfoBar(player),
        uiElement : changedFields,
        availableActions : player.availableActions,
        cardsToAuction : this.cardsToAuction,
        incomeReceipt : incomeReceipt
    };    
}

Game.prototype.setBidding = function(player, bid) {
    logger.debug("[setBidding] Accepted bid for player "+player.playerName+" : -"+bid+"-");
    this.biddings[player._id] = { acutalCost: 0, originalBid: bid};    
}

Game.reinit = function(body) {
    body.__proto__ = Game.prototype;
    CardStack.reinit(body.cardsCommercial);
    CardStack.reinit(body.cardsMisc);
    GameField.reinit(body.gameField);
    MonthHelper.reinit(body.currentDate);    
}

module.exports = Game;