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 @@
@@ -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)
+ })
+ }
+}