antiyoy.js/Game.js

550 lines
18 KiB
JavaScript
Raw Permalink Normal View History

2019-04-08 19:54:07 +00:00
/**
* @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);
}
}