diff --git a/src/index.html b/src/index.html index 8c80864..cd9f312 100644 --- a/src/index.html +++ b/src/index.html @@ -18,8 +18,10 @@ + + @@ -28,9 +30,9 @@ - + @@ -77,7 +79,7 @@
- LOGS + INFO
@@ -279,7 +281,20 @@
- +
+ Nr of +
+
+ Similarity: + +
+
+ Best: +
+
Worst:
+
    + +
diff --git a/src/logic/agent.js b/src/logic/agent.js index 260d520..ab30ce1 100644 --- a/src/logic/agent.js +++ b/src/logic/agent.js @@ -10,8 +10,7 @@ class Agent extends AgentController { const cycle = async () => { const jobRequest = Products.instance.getJobRequest(); if (jobRequest) { - const requiredAmount = 50 - jobRequest.amount; - await this.takeFromStore(jobRequest.product, requiredAmount); + await this.takeFromStore(jobRequest.product, jobRequest.amount); await this.deliver(jobRequest.x, jobRequest.y); } else { await waitFor(1000); @@ -44,7 +43,7 @@ class Agent extends AgentController { Products.instance.exchange(x, y, this.ownedProduct, 50); this.ownedProductAmount = 0; - this.ownedProduct = Product.REGISTRY.empty; + this.ownedProduct = null; } /** diff --git a/src/logic/costMap.js b/src/logic/costMap.js index 5419f3e..26fb74a 100644 --- a/src/logic/costMap.js +++ b/src/logic/costMap.js @@ -36,43 +36,3 @@ class CostMap { } } } - -class CostMapVisualisation { - static instance - - constructor(canvas) { - this.canvas = canvas - this.ctx = canvas.getContext('2d'); - canvas.width = 2000 - canvas.height = 900 - - CostMapVisualisation.instance = this - - document.addEventListener('keydown', ({ key }) => { - if (key !== 'c') - return - - if (this.hasBeenUpdated) { - this.ctx.clearRect(0, 0, 2000, 900); - } else { - this.update(); - } - - this.hasBeenUpdated = !this.hasBeenUpdated - }) - } - - update() { - CostMap.instance.values.forEach((cost, i) => { - const y = Math.floor(i / 20) * 100; - const x = (i % 20) * 100; - - let color = Math.floor((cost * 255)).toString(16) - if (color.length == 1) - color = "0" + color - - this.ctx.fillStyle = '#' + color + color + color; - this.ctx.fillRect(x, y, 100, 100) - }) - } -} diff --git a/src/logic/evolution.js b/src/logic/evolution.js new file mode 100644 index 0000000..55f8fb3 --- /dev/null +++ b/src/logic/evolution.js @@ -0,0 +1,251 @@ +const ShelfCount = 17 * 6; +const InRow = 3 * 6; + +// index to abstract coordinates +function i2ac(index) { + const y = Math.floor(index / InRow); + const x = (index % InRow); + if (y === 5 && x >= 6) + return [x + 6, y]; + return [x, y]; +} + +function ac2i(x, y) { + return y * InRow + x - (y === 5 && x >= 6) * 6; +} + +// index to grid coordinates +function i2gc(index) { + const [x, y] = i2ac(index); + return [x + Number(x > 11) + Number(x > 5), y + Math.floor((y + 1) / 2)]; +} + +function gc2i(x, y) { + x -= Number(x >= 13) + Number(x >= 6); + switch (y) { + case 0: y = 0; break; + case 2: y = 1; break; + case 3: y = 2; break; + case 5: y = 3; break; + case 6: y = 4; break; + case 8: y = 5; break; + } + return ac2i(x, y); +} + +class Arrangement { + generateRandom() { + this.products = [...Array(ShelfCount).keys()].map(() => Knowledge.semanticNetwork.getRandom().name); + this.productsAmount = null; + this.similarities = null; + } + + constructor(products = []) { + this.products = products; + this.productsAmount = null; + this.similarities = null; + } + + clone() { + return JSON.parse(JSON.stringify(this.products)); + } + + simulate() { + this.similarities ||= [...Array(ShelfCount)].map(() => 0); + this.productsAmount ||= [...Array(ShelfCount)].map(() => 50); + + this.similarity = 0; + this.products.forEach((product, i) => { + let similarityWithNeighbours = 0; + const neighbours = neighboursByIndex(i); + for (const neighbour of neighbours) { + similarityWithNeighbours += similarity(product, this.products[neighbour]); + } + similarityWithNeighbours /= neighbours.length; + this.similarities[i] = 1 - similarityWithNeighbours; + this.similarity += this.similarities[i]; + }); + + this.similarity /= ShelfCount; + } +} + +const PopulationSize = 31 * 2; +const PopulationToKeep = 0.3; +const MutationRate = 0.2; + +class Evolution extends Arrangement { + constructor() { + super(); + this.generate(); + this.setup(); + this.productIndex = 0; + this.productsAmount = [...Array(ShelfCount)].map(() => 0); + } + + generate() { + this.population = [...Array(PopulationSize)].map(() => { + const arr = new Arrangement(); + arr.generateRandom(); + return arr; + }); + } + + setup() { + this.populationIndex = 0; + this.population.forEach(x => x.simulate()); + this.min_similarity = this.population.reduce((p, c) => Math.min(p, c.similarity || 1), 1); + this.max_similarity = this.population.reduce((p, c) => Math.max(p, c.similarity || 0), 0); + } + + next(days) { + for (let i = 0; i < Number(days) - 1; ++i) { + this.step(false); + } + this.step(true); + } + + step(render = true) { + // Complete previous day + this.products = this.population[this.populationIndex].products; + this.simulate(); + this.productsAmount = [...Array(ShelfCount)].map((_, i) => Math.round(50 * this.similarities[i])); + const productsSold = ShelfCount * 50 - this.productsAmount.reduce((p, c) => p + c, 0); + + // Setup new day + this.populationIndex++; + this.productIndex = 0; + + // Generate new population if needed + if (this.populationIndex >= PopulationSize) { + /** + * @type{Arrangement[]} + */ + let newPopulation = []; + this.population.sort((a, b) => a.similarity - b.similarity); + + let fitness = this.population.map(({ similarity }) => similarity); + let sum = fitness.reduce((p, c) => p + c, 0); + fitness = fitness.map(f => Math.pow(f / sum, 2)); + + const D = PopulationToKeep * PopulationSize; + + for (let i = 0; i < this.population.length; ++i) { + const candidateA = this.population[pick(fitness)]; + const candidateB = this.population[pick(fitness)]; + const candidate = crossover(candidateA, candidateB); + mutate(candidate); + newPopulation.push(candidate); + } + + this.population = this.population.slice(0, D).concat(newPopulation).slice(0, PopulationSize); + this.setup(); + } + + if (render) { + this.update(); + console.log(productsSold) + } + } + + getJobRequest() { + if (this.productIndex > ShelfCount) + return null; + + for (; this.productIndex < ShelfCount; ++this.productIndex) { + const inPopulation = this.population[this.populationIndex]; + let amount = 50; + if (this.products[this.productIndex] === inPopulation.products[this.productIndex]) { + if (this.productsAmount[this.productIndex] == 50) + continue; + amount -= this.productsAmount[this.productIndex]; + } + const [x, y] = i2gc(this.productIndex++); + return { + product: Knowledge.semanticNetwork.findProductByName(this.population[this.populationIndex].products[this.productIndex]), + amount, + x, y + } + } + } + + getSimilarity() { + try { + this.population[this.populationIndex].simulate(); + return this.population[this.populationIndex].similarity; + } catch (e) { + return this.similarity; + } + } +} + +/** + * + * @param {Arrangement} arr + */ +function mutate(arr) { + const { floor, random } = Math; + for (let i = 0; i < ShelfCount / 6; ++i) { + while (random() <= MutationRate) { + const a = floor(random() * arr.products.length); + const b = floor(random() * arr.products.length); + + if (a !== b) { + let t = arr.products[a]; + arr.products[a] = arr.products[b]; + arr.products[b] = t; + } + } + } +} +/** + * @param {Arrangement} ca + * @param {Arrangement} cb + */ +function crossover(ca, cb) { + const child = []; + for (let i = 0; i < ca.products.length; ++i) + child.push(ca.similarities[i] > cb.similarities[i] ? ca.products[i] : cb.products[i]); + return new Arrangement(child); +} + +function neighboursByIndex(index) { + const [bx, by] = i2ac(index); + const at = (dx, dy) => { + const x = bx + dx; + const y = by + dy; + if (by != y) { + if ((by === 1 || by === 3) && (by - y < 0)) return []; + if ((by === 2 || by === 4) && (by - y > 0)) return []; + } + if (x >= 0 && y >= 0) { + const index = ac2i(x, y); + if (index < ShelfCount) + return [index]; + } + return []; + }; + + return [at(0, -1), at(-1, 0), at(1, 0), at(0, 1)].flat(); +} + + +// given two products, find how similar they are based on categories +// similiary is defined as euclidian distance +function similarity(leftProductName, rightProductName) { + const lhs = Knowledge.semanticNetwork.findByName(leftProductName); + const rhs = Knowledge.semanticNetwork.findByName(rightProductName); + console.assert(lhs && rhs); + + const categories = new Set([...lhs.categories, ...rhs.categories]); + const d = 1 / categories.size; + + let similarity = 0; + for (const category of categories) { + similarity += Math.pow( + (lhs.categories.indexOf(category) >= 0 ? d : 0) - + (rhs.categories.indexOf(category) >= 0 ? d : 0), 2); + } + + return Math.sqrt(similarity); +} diff --git a/src/logic/time.js b/src/logic/time.js index 0cd06f1..762b696 100644 --- a/src/logic/time.js +++ b/src/logic/time.js @@ -1,6 +1,6 @@ class Time { - isPaused = false; + isPaused = true; timeMultiplier = 1; day = 0; @@ -58,7 +58,13 @@ class Time { * Add days */ addDays(days) { + Products.instance.next(days); + Products.instance.update(); + const ul = document.querySelector('ul#history'); this.setDay(this.day += Number(days)); + const li = document.createElement('li'); + li.innerHTML = `${this.day}: ${nice(Products.instance.max_similarity)}`; + ul.appendChild(li); } } \ No newline at end of file diff --git a/src/main.js b/src/main.js index 1fdd589..1326ab4 100644 --- a/src/main.js +++ b/src/main.js @@ -10,12 +10,11 @@ window.addEventListener('DOMContentLoaded', async () => { const costMap = new CostMap(); const time = new Time(); const timeController = new TimeController(); - const costMapVisualisation = new CostMapVisualisation(costCanvas); const floor = new Floor(floorCanvas); const grid = new Grid(gridCanvas); const agent = new Agent(agentCanvas); - await Knowladge.loadFromFile('/data/products.tsv') + await Knowledge.loadFromFile('/data/products.tsv') const products = new Products(productsCanvas); const fpsElement = document.getElementById('fps'); diff --git a/src/styles.css b/src/styles.css index ce2822b..d529899 100644 --- a/src/styles.css +++ b/src/styles.css @@ -107,6 +107,8 @@ main { height: 100%; width: 100%; overflow: auto; + padding: 10px; + box-sizing: border-box; } .logs-content::-webkit-scrollbar { diff --git a/src/utilities.js b/src/utilities.js index 8685aa9..614b4d2 100644 --- a/src/utilities.js +++ b/src/utilities.js @@ -41,3 +41,13 @@ function Enum(...labels) { } return e } + +function pick(array) { + let index = 0; + for (let r = Math.random(); r >= 0; r -= array[index]) { + index++; + } + return index - 1; +} + +function nice(v) { return `${(v * 100).toFixed(1)}%` } \ No newline at end of file diff --git a/src/view/agentController.js b/src/view/agentController.js index 23afc88..56a6c3b 100644 --- a/src/view/agentController.js +++ b/src/view/agentController.js @@ -4,7 +4,7 @@ class AgentController { this.ctx = canvas.getContext('2d'); this.setCanvasSize(canvas); - this.ownedProduct = Product.REGISTRY.empty; + this.ownedProduct = null; this.ownedProductAmount = 0; // Akcje @@ -242,17 +242,19 @@ class AgentController { this.ctx.rotate(-rotation * Math.PI / 180); // Product - this.ctx.font = '900 40px "Font Awesome 5 Free"'; - this.ctx.textAlign = 'center'; - this.ctx.fillStyle = 'white'; - this.ctx.fillText(this.ownedProduct.icon, 0, 15); + if (this.ownedProduct) { + this.ctx.font = '900 40px "Font Awesome 5 Free"'; + this.ctx.textAlign = 'center'; + this.ctx.fillStyle = 'white'; + this.ctx.fillText(this.ownedProduct.icon, 0, 15); - // Amount - if (this.ownedProductAmount) { - this.ctx.font = '20px Montserrat'; - this.ctx.textAlign = 'left'; - this.ctx.fillStyle = 'white'; - this.ctx.fillText(this.ownedProductAmount, 10, 35); + // Amount + if (this.ownedProductAmount) { + this.ctx.font = '20px Montserrat'; + this.ctx.textAlign = 'left'; + this.ctx.fillStyle = 'white'; + this.ctx.fillText(this.ownedProductAmount, 10, 35); + } } this.ctx.translate(-(newX + (w / 2)), -(newY + (h / 2))); diff --git a/src/view/products.js b/src/view/products.js index 2055894..cda11ed 100644 --- a/src/view/products.js +++ b/src/view/products.js @@ -9,38 +9,20 @@ function filterReduce2D(array, reducer, filter, init) { return result; } -class Products { +class Products extends Evolution { + /** + * @type{Products} + */ static instance constructor(canvas) { - const { random, floor } = Math - - this.gridProducts = [] - this.gridProductsAmount = [] - this.min = 50; - - for (let i = 0; i < 20; ++i) { - const gridProductsColumn = [] - const gridProductsAmountColumn = [] - - for (let j = 0; j < 9; ++j) { - if (j % 3 !== 1 && (i+1) % 7 !== 0) { - gridProductsColumn.push(Product.RANDOM_FROM_REGISTRY()); - const amount = floor(random() * 51); - gridProductsAmountColumn.push(amount); - this.min = this.min > amount ? amount : this.min; - } else { - gridProductsColumn.push(''); - gridProductsAmountColumn.push(''); - } - } - - this.gridProducts.push(gridProductsColumn); - this.gridProductsAmount.push(gridProductsAmountColumn); - } + super(); + // this.generateRandom(); + this.ctx = canvas.getContext('2d'); this.setCanvasSize(canvas); + this.update(); Products.instance = this @@ -51,33 +33,47 @@ class Products { canvas.height = 900; } - update(){ + update() { + function write(tag, text) { + const qr = document.querySelector(tag); + if (qr) { + qr.innerHTML = text; + } + } + + + + write('#similarity', nice(this.getSimilarity())); + write('#populationIndex', this.populationIndex + 1); + write('#populationSize', PopulationSize); + write('#best', nice(this.max_similarity)); + write('#worst', nice(this.min_similarity)); + this.clearCanvas(); this.drawProducts(); } - drawProducts () { - for (let [x, line] of Grid.instance.grid.entries()) { - for (let [y, type] of line.entries()) { - if (type === GRID_FIELD_TYPE.SHELF) { - let v = this.drawValue(x, y); - this.product(x * 100, y * 100, v, x, y); - } - } - } + drawProducts() { + this.min = this.productsAmount?.reduce((p, c) => Math.min(p, c), 50) || 50; + this.products.forEach((product, index) => { + let [x, y] = i2gc(index); + x *= 100; y *= 100; + const amount = this.productsAmount === null ? 50 : this.productsAmount[index]; + this.drawAmount(x, y, amount); + const { icon } = Knowledge.semanticNetwork.findProductByName(product); + this.product(x, y, icon, amount); + }) } - drawValue(x, y) { + drawAmount(x, y, amount) { let fontSize = 20; this.ctx.font = `${fontSize}px Montserrat`; this.ctx.textAlign = "left"; this.ctx.fillStyle = 'white'; - let number = this.gridProductsAmount[x][y]; - this.ctx.fillText(number, (x * 100) + 10, (y * 100) + 90); - return number; + this.ctx.fillText(amount, x + 10, y + 90); } - product(x, y, v, productsKey, productsValue) { + product(x, y, icon, amount) { let fontSize = 40; this.ctx.font = `900 ${fontSize}px "Font Awesome 5 Free"`; this.ctx.textAlign = "center"; @@ -86,17 +82,15 @@ class Products { // white - full shelf // red - empty shelf const t = Math.cbrt - v = (v - this.min) / (50 - this.min); + const v = (amount - this.min) / (50 - this.min); let color = Math.floor(t(v) * 255).toString(16) if (color.length == 1) color = "0" + color this.ctx.fillStyle = `#ff${color}${color}`; - let productsColumn = this.gridProducts[productsKey]; - let prdct = productsColumn[productsValue]; this.ctx.font = '900 40px "Font Awesome 5 Free"'; this.ctx.textAlign = 'center'; - this.ctx.fillText(prdct.icon, x+50, y+60); + this.ctx.fillText(icon, x + 50, y + 60); } clearCanvas() { @@ -142,13 +136,13 @@ class Products { * @returns {{product: Product; amount: number} | null } Old shelf content */ exchange(x, y, incomingProduct, incomingAmount) { - const product = this.gridProducts[x][y]; - const amount = this.gridProductsAmount[x][y]; - this.gridProducts[x][y] = incomingProduct; - this.gridProductsAmount[x][y] = incomingAmount; + const index = gc2i(x, y); + const product = this.products[index]; + const amount = this.productsAmount[index]; + this.products[index] = incomingProduct.name; + this.productsAmount[index] = incomingAmount; - this.min = filterReduce2D(this.gridProductsAmount, (p, c) => Math.min(p, c), - x => typeof x === 'number', 50); + this.min = this.productsAmount?.reduce((p, c) => Math.min(p, c), 50) || 50; this.clearCanvas(); this.drawProducts(); @@ -170,13 +164,6 @@ class Products { return this.exchange(x, y, incomingProduct, incomingAmount) } - getJobRequest() { - const maybeProduct = this.filter((_product, amount, _i, _j, end) => amount < 50).sort((a, b) => { - return a.amount - b.amount - }) - return maybeProduct.length === 0 ? null : maybeProduct[0] - } - findAvailableLocationsFor(product) { return this.filter((p, amount) => product.equals(p) && amount < 50) } diff --git a/src/view/visualisation.js b/src/view/visualisation.js new file mode 100644 index 0000000..8cf0ad7 --- /dev/null +++ b/src/view/visualisation.js @@ -0,0 +1,53 @@ +class Visualisation { + static instance + + constructor(canvas) { + this.canvas = canvas + this.ctx = canvas.getContext('2d'); + canvas.width = 2000 + canvas.height = 900 + + Visualisation.instance = this + + document.addEventListener('keydown', ({ key }) => { + const keys = { + c: () => this.drawCostMap(), + p: () => this.drawSimilarities(), + }; + + this.ctx.clearRect(0, 0, 2000, 900); + if (this.lastkey != key) { + for (const k in keys) + if (k == key) { + keys[k](); + this.lastkey = key; + } + } else + this.lastkey = null; + }) + } + + drawSimilarities() { + Products.instance.similarities.map((similarity, index) => { + const [x, y] = i2gc(index); + + let color = Math.floor(similarity * 120); + this.ctx.fillStyle = `hsl(${color}, 100%, 50%)`; + this.ctx.fillRect(x * 100, y * 100, 100, 100) + }) + } + + drawCostMap() { + CostMap.instance.values.forEach((cost, i) => { + const y = Math.floor(i / 20) * 100; + const x = (i % 20) * 100; + + let color = Math.floor((cost * 255)).toString(16) + if (color.length == 1) + color = "0" + color + + this.ctx.fillStyle = '#' + color + color + color; + this.ctx.fillRect(x, y, 100, 100) + }) + } +}