/** * @property {Node} btnTroops * @property {Node} btnFarmer * @property {Node} btnHoplite * @property {Node} btnServant * @property {Node} btnKnight * @property {Node} btnTowers * @property {Node} btnFarm * @property {Node} btnTowerSmall * @property {Node} btnTowerBig */ class Game { constructor() { this.fieldSize = {x: 20, y: 10}; this.scale = 1; this.deltaX = 0; this.deltaY = 0; this.warriors = {}; this.buildings = {}; this.field = []; this.civilizations = {}; this.trees = {}; this.round = 1; this.hexagonSize = {width: 27.85714285714286, padding: 32.16760145166612, offset: 6.9285714285}; this.initializeElements(); this.resize(); this.createHexagons(); this.registerEventListener(); } initializeElements() { this.elements = {}; this.container = body.createChild('div', 'gameContainer'); this.gameStats = this.container.createChild('div', 'gameStats'); this.viewContainer = this.container.createChild('div', 'viewContainer'); this.view = this.viewContainer.createChild('div', 'view'); this.playground = this.view.createChild('ul', 'playground'); this.btns = this.container.createChild('div', 'controls'); this.elements.roundParent = this.gameStats.createChild('h3', ['gameStat', 'left'], 'Round: '); this.elements.round = this.elements.roundParent.createChild('span', 'count', '1'); this.elements.balanceParent = this.gameStats.createChild('h3', ['gameStat', 'center']); this.elements.balance = this.elements.balanceParent.createChild('span', 'count'); this.elements.balanceChange = this.elements.balanceParent.createChild('span', ['count', 'text-grey']); this.elements.player = this.gameStats.createChild('h3', ['gameStat', 'right'], 'Player ' + player.color.toUpperCase()); this.nextRoundBtn = this.btns.createChild('button', ['btn', 'btn-primary', 'btn-nextRound'], 'Next Round'); this.nextRoundBtn.addEventListener('click', () => { this.nextRound() }); [this.btnTroops, this.btnFarmer, this.btnHoplite, this.btnServant, this.btnKnight] = createTroopsBtn(this.btns); [this.btnFarmer, this.btnHoplite, this.btnServant, this.btnKnight].forEach((btn, index) => { const types = [Farmer, Hoplite, Servant, Knight]; this.addBtnClickListener(btn, types[index], 'addWarrior'); }); [this.btnTowers, this.btnFarm, this.btnTowerSmall, this.btnTowerBig] = createTowersBtn(this.btns); [this.btnFarm, this.btnTowerSmall, this.btnTowerBig].forEach((btn, index) => { const types = [Farm, SmallTower, BigTower]; this.addBtnClickListener(btn, types[index], 'addBuilding'); }); } addBtnClickListener(btn, className, functionName) { btn.addEventListener('click', () => { if (!!this.selectedCivilization) { const newObj = { type: 'new', class: className, function: functionName, civilization: this.selectedCivilization }; if (!!this.draggingElement && !!this.selectedObject && this.selectedObject.type === 'new' && this.selectedObject.class === newObj.class && this.selectedObject.civilization === newObj.civilization) return; this.deselectObject(); this.draggingElement = className.getDraggingObject(this); this.selectedObject = newObj; } }); } addBuilding(x, y, civilization, type) { const tile = this.field[x][y]; console.log(tile, type); if (type.initialCosts > civilization.balance) return false; console.log('creatio'); if (!tile.claimedBy || tile.claimedBy.player !== civilization.player) // Is non-friendly territory return false; if (tile.object !== null) return false; console.log('success'); const building = new type(x, y, this, civilization); tile.object = building; this.buildings[building.uuid] = building; return true; } addCivilization(player, x, y, realPlayer = false, addEnvironment = true, addTownhall = true) { if (!this.civilizations[player.uuid]) this.civilizations[player.uuid] = []; const CivilizationType = realPlayer ? Civilization : CivilizationAI; const newCivilization = new CivilizationType(player, this, x, y, addEnvironment, addTownhall); this.civilizations[player.uuid].push(newCivilization); return newCivilization; } addTree(x, y) { if (!!this.field[x][y].object) return; const tree = new Tree(x, y, this); this.trees[tree.uuid] = tree; } addWarrior(x, y, civilization, type) { const tile = this.field[x][y]; if (type.initialCosts > civilization.balance) return false; const warrior = new type(x, y, this, civilization); const player = civilization.player; if (!!tile.claimedBy && tile.claimedBy.player === player) { // Is friendly territory if (!!tile.object) { // Is object on the tile? if (tile.object instanceof Tree) { tile.object.cutDown(); } else if (tile.object instanceof Warrior) { if (tile.object.rank <= 4 - warrior.rank) { tile.object.merge(warrior); } return false; } else if (tile.object instanceof Building) { return false; } } } else { // Is hostile territory if (!tile.getAdjacentTiles().find(adjTile => adjTile.claimedBy === civilization)) { // There isn't any tile of the given civilization nearby return false; } // TODO: Check adjacent tiles if (!!tile.object) { if (tile.object instanceof Tree) { tile.object.cutDown(); } else if (tile.object instanceof Warrior) { if (tile.object.rank < warrior.rank) { tile.object.kill(); } else { return false; } } else if (tile.object instanceof Building) { if (tile.object.rank < warrior.rank) { tile.object.destroy(); } else { return false; } } } } this.warriors[warrior.uuid] = warrior; tile.object = warrior; tile.overtake(civilization); return true; } createHexagons() { for (let x = 0; x < this.fieldSize.x; x++) { this.field.push([]); } const margin = Math.round(this.hexagonSize.width * .1); for (let y = 0; y < this.fieldSize.y; y++) { for (let x = 0; x < this.fieldSize.x; x++) { const listItem = this.playground.createChild('li'); listItem.style.width = this.hexagonSize.width + "px"; listItem.style.marginRight = margin + 'px'; listItem.style.paddingBottom = this.hexagonSize.padding + "px"; if (y % 2 !== 0) { if (x === 0) listItem.style.marginLeft = margin / 2 + 'px'; listItem.style.marginTop = listItem.style.marginBottom = -2 * margin + "px"; listItem.classList.add('even-row'); } listItem.createChild('div', ['hexagon', 'grey']); this.field[x][y] = new Tile(this, listItem, x, y); } } } calculateHexagonSize() { const paddingRatio = this.hexagonSize.padding / this.hexagonSize.width; const offsetRatio = this.hexagonSize.offset / this.hexagonSize.width; this.container.style.width = ''; const widthPercentage = (100 / this.fieldSize.x * .9); let width = Math.round(widthPercentage / 100 * this.container.clientWidth); if (width % 2 !== 0) width -= 1; this.hexagonSize.width = width; this.hexagonSize.padding = this.hexagonSize.width * paddingRatio; this.hexagonSize.offset = this.hexagonSize.width * offsetRatio; } increaseRound() { this.round++; this.elements.round.innerText = this.round; } nextRound() { // TODO: Simulate other players // Lets the trees breed Object.values(this.trees).forEach(tree => { tree.breed(); }); // Handles finances of the civilizations Object.values(this.civilizations).forEach(civilizations => { civilizations.forEach(civilization => { civilization.processRound(); }); }); // Sets all warriors waiting Object.values(this.warriors).forEach(warrior => { if (warrior.civilization.player === player) { warrior.setNeedsAction(true); } }); this.increaseRound(); this.updateBalance(); } registerEventListener() { let mouseStartX; let mouseStartY; let startX; let startY; this.view.onmousedown = e => { mouseStartX = e.clientX; mouseStartY = e.clientY; startX = parseInt(this.view.style.left.slice(0, this.view.style.left.length - 2)) || 0; startY = parseInt(this.view.style.top.slice(0, this.view.style.top.length - 2)) || 0; }; this.view.onmousemove = e => { if (!!mouseStartX && !!mouseStartY) { let deltaX = e.clientX - mouseStartX + startX; let deltaY = e.clientY - mouseStartY + startY; this.repositionView(deltaX, deltaY); } }; this.view.onmouseup = e => { mouseStartX = undefined; mouseStartY = undefined; }; this.viewContainer.onwheel = e => { const prevScale = this.scale; if (e.deltaY < 0) { this.scale *= 1.1; } else { this.scale /= 1.1; } this.scale = Math.max(this.scale, 1); this.scale = Math.min(this.scale, 10); const deltaScale = this.scale / prevScale; this.view.style.transform = `scale(${this.scale})`; if (!!this.draggingElement) { let dragging = this.draggingElement.childNodes[0]; dragging.style.width = deltaScale * dragging.clientWidth + 'px'; dragging.style.height = deltaScale * dragging.clientHeight + 'px'; } let centerY = -(this.view.clientHeight - this.viewContainer.clientHeight); if (this.view.clientHeight < this.viewContainer.clientHeight) { centerY /= 2; } this.view.classList.add('zooming'); this.repositionView(this.deltaX * deltaScale, centerY - (centerY - this.deltaY) * deltaScale); setTimeout(() => this.view.classList.remove('zooming'), 200); }; document.onmousemove = (ev) => { if (!!this.draggingElement) { const draggingContainer = this.draggingElement; const dragging = draggingContainer.childNodes[0]; draggingContainer.style.left = ev.clientX + 'px'; draggingContainer.style.top = ev.clientY + 'px'; if (!!prevMouseEvent) { clearTimeout(timeout); const vel = (ev.clientX - prevMouseEvent.clientX) / (ev.timeStamp - prevMouseEvent.timeStamp); let rotation = Math.min(Math.max(vel * 80, -80), 80); dragging.style.transform = 'rotateZ(' + rotation + 'deg)'; timeout = setTimeout(() => { dragging.style.transform = 'rotateZ(0deg)'; }, 50); } prevMouseEvent = ev; } }; this.container.addEventListener('contextmenu', (ev) => ev.preventDefault()); window.addEventListener('resize', () => this.resize()); } repositionView(deltaX, deltaY) { const maxDeltaX = (this.view.clientWidth * this.scale - this.viewContainer.clientWidth) / 2; const maxDeltaTop = this.view.clientHeight * (this.scale - 1) / 2; let maxDeltaBottom = (this.view.clientHeight - this.viewContainer.clientHeight); if (this.view.clientHeight < this.viewContainer.clientHeight) { maxDeltaBottom /= 2; } deltaX = Math.min(deltaX, maxDeltaX); deltaX = Math.max(deltaX, -maxDeltaX); deltaY = Math.min(deltaY, maxDeltaTop); deltaY = Math.max(deltaY, -maxDeltaBottom); this.deltaX = deltaX; this.deltaY = deltaY; this.view.style.left = deltaX + 'px'; this.view.style.top = deltaY + 'px'; } resize() { this.calculateHexagonSize(); const containerWidth = Math.round(this.hexagonSize.width / .9) * this.fieldSize.x; this.container.style.width = containerWidth + 'px'; const margin = Math.round(this.hexagonSize.width * .1); this.field.forEach((col, x) => { col.forEach((tile, y) => { tile.element.style.width = this.hexagonSize.width + "px"; tile.element.style.marginRight = margin + 'px'; tile.element.style.paddingBottom = this.hexagonSize.padding + "px"; if (y % 2 !== 0) { if (x === 0) tile.element.style.marginLeft = margin / 2 + 'px'; tile.element.style.marginTop = tile.element.style.marginBottom = -2 * margin + "px"; } }); }); // this.view.style.width = containerWidth + this.hexagonSize.width / 2 + 'px'; this.view.style.height = (this.hexagonSize.width / 1.01 * this.scale) * this.fieldSize.y + 'px'; this.field.forEach(col => { col.forEach(tile => { if (!!tile.object) { tile.object.adjustPosition(); } }); }); this.repositionView(0, 0); } selectCivilization(civilization) { if (!!this.selectedCivilization) this.selectedCivilization.select(false); if (!!civilization && civilization.player === player) { this.selectedCivilization = civilization; civilization.select(true); this.btnTroops.classList.remove('disabled'); this.btnTowers.classList.remove('disabled'); } else { this.selectedCivilization = null; this.btnTroops.classList.add('disabled'); this.btnTroops.classList.remove('isActive'); this.btnTowers.classList.add('disabled'); this.btnTowers.classList.remove('isActive'); } this.updateBalance(); } selectWarrior(warrior) { this.selectedObject = warrior; } updateBalance() { if (!!this.selectedCivilization && this.selectedCivilization.player === player) { const balanceChange = this.selectedCivilization.getBalanceChange(); this.elements.balance.innerText = this.selectedCivilization.balance; this.elements.balanceChange.innerText = balanceChange > 0 ? '+' + balanceChange : balanceChange; } else { this.elements.balance.innerText = null; this.elements.balanceChange.innerText = null; } } deselectObject() { if (!!this.selectedObject) { this.selectedObject.isSelected = false; } this.selectedObject = null; if (!!this.draggingElement) { this.draggingElement.parentNode.removeChild(this.draggingElement); } this.draggingElement = null; } } class CivilizationNN extends Civilization { constructor(player, game, x, y) { super(player, game, x, y, true, true); } } class GameNN extends Game { constructor(nn) { super(); this.ga = nn || new GeneticAlgorithm(10, 4, this); for (let i = 0; i < this.ga.maxUnits; i++) { let x; let y; let isGood = false; while(!isGood) { x = this.ga.random(0, 19); y = this.ga.random(0, 9); if (!this.field[x][y].getAdjacentTiles().find(tile => !!tile.claimedBy)) isGood = true; } const civ = this.addCivilization(new Player('green'), x, y); civ.index = i; } this.initialCivs = this.civilizations; this.fotze = true; if(!nn) { this.ga.reset(); this.ga.createPopulation(); } } addCivilization(player, x, y) { if (!this.civilizations[player.uuid]) this.civilizations[player.uuid] = []; const newCivilization = new CivilizationNN(player, this, x, y); this.civilizations[player.uuid].push(newCivilization); if(x < this.fieldSize.x - 1) this.addWarrior(x + 1, y, newCivilization, Farmer); return newCivilization; } nextRound() { if(Object.values(this.civilizations).length === 0) { this.ga.evolvePopulation(); this.ga.iteration++; } Object.values(this.civilizations).forEach(civilizations => { civilizations.forEach(civilization => { civilization.fitness = civilization.getSize(); civilization.score = this.round; this.ga.activateBrain(civilization); }); }); super.nextRound(); if(this.round === 1000) { this.ga.evolvePopulation(); this.ga.iteration++; Object.values(this.civilizations).forEach(player => { player.forEach(civ => { civ.destroy(); }); }); this.civilizations = this.initialCivs; Object.values(this.civilizations).forEach(player => { player.forEach(civ => { civ.spawn(); }); }); this.round = 0; } setTimeout(() => { if (this.fotze) this.nextRound(); }, 50); } }