550 lines
18 KiB
JavaScript
550 lines
18 KiB
JavaScript
/**
|
|
* @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);
|
|
}
|
|
} |