From 82c3de94ef8bb8453e191737907a76c33a13f52f Mon Sep 17 00:00:00 2001 From: AgataWojciech Date: Fri, 18 Jun 2021 19:59:06 +0200 Subject: [PATCH 1/3] =?UTF-8?q?Dodano=20=C5=82adowanie=20pliku=20wiedzy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/data/products.tsv | 2 +- src/logic/knowledge.js | 100 ++++++++++++++++---------------- src/logic/product.js | 127 ----------------------------------------- src/main.js | 8 ++- 4 files changed, 57 insertions(+), 180 deletions(-) diff --git a/src/data/products.tsv b/src/data/products.tsv index ec5df2b..b806bb6 100644 --- a/src/data/products.tsv +++ b/src/data/products.tsv @@ -1,4 +1,4 @@ -name icon category +name icon categories airFrashener fa-wind home accessories apple fa-apple-alt food, fruits, fresh babyAccessories fa-baby-carriage kids, accessories, hygiene diff --git a/src/logic/knowledge.js b/src/logic/knowledge.js index 8072414..b53eeae 100644 --- a/src/logic/knowledge.js +++ b/src/logic/knowledge.js @@ -6,31 +6,20 @@ function flattenToUnique(array, view) { }, new Set()) } +function intoProduct({ name, icon }) { + return new Product(name, icon) +} + class SemanticNetwork { /** - * @param {string} definition + * @param {{ + * name: string; + * icon: string; + * categories: string[]; + * }[]} data */ - constructor(definition) { - /** - * @param {string} stmt - * @returns {[{ name: string; categories: string[]; locations: string[] }]} - */ - function parseStatement(stmt) { - const [name, ...keywords] = stmt.split(':').map(x => x.trim()) - const o = { name } - for (const keyword of keywords) { - const command = keyword[0] - const items = keyword.slice(1).split(',').map(x => x.trim()).filter(x => x.length > 0) - switch (command) { - case 'c': o.categories = items; break - case 'p': o.locations = items; break - } - } - return o - } - - this.data = definition.split('\n').filter(x => x.trim().length > 0) - .map(parseStatement) + constructor(data) { + this.data = data; } getAllNames() { @@ -41,10 +30,6 @@ class SemanticNetwork { return flattenToUnique(this.data, ({ categories }) => categories) } - getAllLocations() { - return flattenToUnique(this.data, ({ locations }) => locations) - } - findByName(nameToFind) { return this.data.find(({ name }) => name === nameToFind) } @@ -53,13 +38,20 @@ class SemanticNetwork { return this.data.filter(({ categories }) => categories.indexOf(categoryToFind) >= 0) } - findAllByLocation(locationToFind) { - return this.data.filter(({ locations }) => locations.indexOf(locationToFind) >= 0) + getRandom() { + const { floor, random } = Math; + return this.data[floor(random() * this.data.length)]; + } + + findProductByName(nameToFind) { + return intoProduct(this.findByName(nameToFind)); + } + + getRandomProduct() { + return intoProduct(this.getRandom()); } } -class AgentSemanticNetwork extends SemanticNetwork {} - function nearbyStorageUnitsCoords(gridX, gridY) { function outsideOfStorageCenter(v) { const { x, y, w, h } = StorageCenterLocation @@ -80,38 +72,45 @@ function nearbyStorageUnitsIndexes(gridX, gridY) { .map(({ x, y }) => x * UnitsCount + y * UnitsCount * RowsOfGroupsCount) } -class Arrangement { - constructor() { - this.products = [...Array(UnitsCount)].map(() => ({ - product: '', - count: 0, - icon: '' - })) - } - - nearbyProducts(gridX, gridY) { - return nearbyStorageUnitsIndexes(gridX, gridY) - .map(i => this.products[i]) - } -} class Knowledge { - static semanticNetwork - static agentSemanticNetwork - static arrangement - constructor(definition) { + /** + * @type{SemanticNetwork} + */ + static semanticNetwork + + static async loadFromFile(path) { + const response = await fetch(path); + const text = await response.text(); + + const [header, ...rows] = text.split('\n') + .map(x => x.trim()) + .filter(x => x) + .map(x => x.split('\t') + .map(cell => cell.split(',').map(x => x.trim()))); + + const data = rows.map(row => + row.reduce((p, c, i) => ({ + ...p, + [header[i]]: i == 2 ? c : c[0] }), {})); + + Knowledge.semanticNetwork = new SemanticNetwork(data); + } + + constructor() { Knowledge.semanticNetwork = new SemanticNetwork(definition) - Knowledge.agentSemanticNetwork = new AgentSemanticNetwork(definition) Knowledge.arrangement = new Arrangement() } } + + /* keywords: p - powiązane pomieszczenia / miejsca c - category -*/ + new Knowledge(` Piłka :c sport,rozrywka, piłka nożna Kubek klubowy :c sport,rozrywka,piłka nożna :p kuchnia @@ -128,3 +127,4 @@ new Knowledge(` Laptop :c elektronika,gaming :p biuro,szkoła Telefon :c elektronika :p biuro,szkoła `) +*/ \ No newline at end of file diff --git a/src/logic/product.js b/src/logic/product.js index 76ebf40..b373edb 100644 --- a/src/logic/product.js +++ b/src/logic/product.js @@ -23,131 +23,4 @@ class Product { equals(other) { return this.name === other.name; } - - static get REGISTRY() { - // in alphabetical order - return { - empty: new Product('', ''), - airFrashener: new Product('airFrashener', 'fa-wind'), - apple: new Product('apple', 'fa-apple-alt'), - babyAccessories: new Product('babyAccessories','fa-baby-carriage'), - bacon: new Product('bacon', 'fa-bacon'), - bandAid: new Product('bandAid', 'fa-band-aid'), - basketballBall: new Product('basketballBall', 'fa-basketball-ball'), - beer: new Product('beer','fa-beer'), - bell: new Product('bell','fa-bell'), - bicycle: new Product('bicycle','fa-bicycle'), - bone: new Product('bone', 'fa-bone'), - book: new Product('book', 'fa-book'), - bread: new Product('bread', 'fa-bread-slice'), - cake: new Product('cake', 'fa-birthday-cake'), - calculator: new Product('calculator', 'fa-calculator'), - calendar: new Product('calendar', 'fa-calendar'), - camera: new Product('camera','fa-camera'), - campAccessories: new Product('campAccessories','fa-campground'), - candyCane: new Product('candyCane', 'fa-candy-cane'), - carrot: new Product('carrot', 'fa-carrot'), - catAccessories: new Product('catAccessories','fa-cat'), - carAccessories: new Product('carAccessories','fa-car'), - cigarettes: new Product('cigarettes', 'fa-smoking'), - chair: new Product('chair', 'fa-chair'), - charger: new Product('charger', 'fa-battery-half'), - cheese: new Product('cheese', 'fa-cheese'), - chess: new Product('chess', 'fa-chess'), - cleaningSupplies: new Product('cleaningSupplies', 'fa-broom'), - cloathesAccessories: new Product('cloathesAccessories','fa-glasses'), - cocktailSupplies: new Product('cocktailSupplies','fa-cocktail'), - coffee: new Product('cofee','fa-coffee'), - contactLenses: new Product('contactLenses', 'fa-eye'), - cookie: new Product('cookie','fa-cookie'), - couch: new Product('couch', 'fa-couch'), - coughSyrop: new Product('coughSyrop', 'fa-lungs'), - cookie: new Product('cookie', 'fa-cookie'), - dictionary: new Product('dictionary', 'fa-language'), - dogAccessories: new Product('dogAccessories','fa-dog'), - drinks: new Product('drinks', 'fa-glass-whiskey'), - drone: new Product('drone','fa-plane'), - drum: new Product('drum', 'fa-drum'), - envelope: new Product('envelope', 'fa-envelope'), - egg: new Product('egg','fa-egg'), - electronicAccessories: new Product('electronicAccessories','fa-bolt'), - eraser: new Product('eraser','fa-eraser'), - extinguisher: new Product('extinguisher','fa-fire'), - firstAid: new Product('firstAid','fa-first-aid'), - fish: new Product('fish','fa-fish'), - gamepad: new Product('gamepad', 'fa-gamepad'), - giftWrapping: new Product('giftWrapping', 'fa-gift'), - glassAccessories: new Product('glassAccessories','fa-wine-glass'), - gps: new Product('gps','fa-street-view'), - guitar: new Product('guitar','fa-guitar'), - hamburger: new Product('hamburger', 'fa-hamburger'), - hammer: new Product('hammer', 'fa-hammer'), - halloweenAccessories: new Product('halloweenAccessories', 'fa-ghost'), - hauntingAccessories: new Product('hauntingAccessories','fa-binoculars'), - headphones: new Product('headphones','fa-headphones'), - highlighter: new Product('highlighter','fa-highlighter'), - homeSecurity: new Product('homeSecurity','fa-key'), - hotdog: new Product('hotdog','fa-hotdog'), - icecream: new Product('icecream','fa-ice-cream'), - insecticide: new Product('insecticide','fa-spider'), - jewelry: new Product('jewelry','fa-gem'), - keyboard: new Product('keyboard', 'fa-keyboard'), - kitchenSupplies: new Product('kitchenSupplies', 'fa-blender'), - laptop: new Product('laptop', 'fa-laptop'), - lemon: new Product('lemon','fa-lemon'), - lightbulb: new Product('lightbulb', 'fa-lightbulb'), - microphone: new Product('microphone', 'fa-microphone'), - mask: new Product('mask', 'fa-head-side-mask'), - monitor: new Product('monitor', 'fa-desktop'), - movingAccessories: new Product('movingAccessories','fa-boxes'), - newbornAccessorries: new Product('newbornAccessorries','fa-baby-carriage'), - newspaper: new Product('newspaper','fa-newspaper'), - paintBrush: new Product('paintBrush','fa-paint-brush'), - paintRoller: new Product('paintRoller', 'fa-paint-roller'), - paper: new Product('paper','fa-paperclip'), - partyAccessories: new Product('partyAccessories','fa-glass-cheers'), - pen: new Product('pen','fa-pen'), - pepper: new Product('pepper','fa-pepper-hot'), - personalSafetyAccessories: new Product('personalSafetyAccessories','fa-fingerprint'), - piggyBank: new Product('piggyBank', 'fa-piggy-bank'), - pills: new Product('pills','fa-pills'), - pizza: new Product('pizza','fa-pizza-slice'), - plants: new Product('plants','fa-seedling'), - printer: new Product('printer','fa-print'), - puzzle: new Product('puzzle', 'fa-puzzle-piece'), - religionAccessories: new Product('religionAccessories','fa-pray'), - ruler: new Product('ruler', 'fa-ruler'), - runningAccessories: new Product('runningAccessories','fa-running'), - sanitizer: new Product('sanitizer', 'fa-spray-can'), - scissors: new Product('scissors', 'fa-cut'), - seeds: new Product('seeds', 'fa-seedling'), - skatingAccessories: new Product('skatingAccessories','fa-skating'), - skiingAccessories: new Product('skiingAccessories','fa-skiing'), - soap: new Product('soap','fa-pump-soap'), - spaAccessories: new Product('spaAccessories','fa-spa'), - sportCloathes: new Product('sportCloathes','fa-futbol'), - stamp: new Product('stamp', 'fa-stamp'), - suitcase: new Product('suitcase', 'fa-suitcase-rolling'), - summerHoliday: new Product('summerHoliday','fa-umbrella-beach'), - sunscreen: new Product('sunscreen','fa-sun'), - swimmingAccessories: new Product('swimmingAccessories','fa-swimmer'), - thermometer: new Product('thermometer', 'fa-thermometer'), - toiletSupplies: new Product('toiletSupplies','fa-toilet-paper'), - tools: new Product('tools','fa-wrench'), - toothpaste: new Product('toothpaste', 'fa-tooth'), - toys: new Product('toys','fa-horse'), - trashBin: new Product('trashBin', 'fa-trash'), - tv: new Product('tv', 'fa-tv'), - umbrella: new Product('umbrella', 'fa-umbrella'), - vinyl: new Product('vinyl', 'fa-record-vinyl'), - volleyballBall: new Product('volleyballBall', 'fa-volleyball-ball'), - wine: new Product('wine','fa-wine-bottle'), - winterCloathes: new Product('winterCloathes','fa-mitten'), - winterHoliday: new Product('winterHoliday','fa-tree') - }; - } - - static RANDOM_FROM_REGISTRY() { - return Product.REGISTRY[Object.keys(Product.REGISTRY).filter(p => p !== 'empty')[Math.floor(Math.random() * (Object.keys(Product.REGISTRY).length - 1))]]; - } } \ No newline at end of file diff --git a/src/main.js b/src/main.js index 30a1b6e..1fdd589 100644 --- a/src/main.js +++ b/src/main.js @@ -1,4 +1,6 @@ -window.addEventListener('DOMContentLoaded', () => document.fonts.load('900 14px "Font Awesome 5 Free"').then(() => { +window.addEventListener('DOMContentLoaded', async () => { + await document.fonts.load('900 14px "Font Awesome 5 Free"') + const floorCanvas = document.getElementById('canvas-floor'); const gridCanvas = document.getElementById('canvas-grid'); const agentCanvas = document.getElementById('canvas-agent'); @@ -12,6 +14,8 @@ window.addEventListener('DOMContentLoaded', () => document.fonts.load('900 14px const floor = new Floor(floorCanvas); const grid = new Grid(gridCanvas); const agent = new Agent(agentCanvas); + + await Knowladge.loadFromFile('/data/products.tsv') const products = new Products(productsCanvas); const fpsElement = document.getElementById('fps'); @@ -20,7 +24,7 @@ window.addEventListener('DOMContentLoaded', () => document.fonts.load('900 14px } else { document.body.removeChild(fpsElement); } -})); +}); function displayFPS(fpsElement) { let old = new Date(); From 8e7b565dd7128547cd6ca520296e7730d2ea612a Mon Sep 17 00:00:00 2001 From: Robert Bendun Date: Sat, 19 Jun 2021 08:45:42 +0200 Subject: [PATCH 2/3] genetic algorithm implementation --- src/index.html | 21 ++- src/logic/agent.js | 5 +- src/logic/costMap.js | 40 ------ src/logic/evolution.js | 251 ++++++++++++++++++++++++++++++++++++ src/logic/time.js | 8 +- src/main.js | 3 +- src/styles.css | 2 + src/utilities.js | 10 ++ src/view/agentController.js | 24 ++-- src/view/products.js | 103 +++++++-------- src/view/visualisation.js | 53 ++++++++ 11 files changed, 402 insertions(+), 118 deletions(-) create mode 100644 src/logic/evolution.js create mode 100644 src/view/visualisation.js 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) + }) + } +} From 4bd5bbdc0a26639410a6634debaff60c8a484593 Mon Sep 17 00:00:00 2001 From: Robert Bendun Date: Sat, 19 Jun 2021 11:05:19 +0200 Subject: [PATCH 3/3] Connected genetic algorithm with decision tree --- src/index.html | 1 + src/logic/arrangment.js | 116 +++++++++++++++++++++++++++++++++++++ src/logic/evolution.js | 83 ++------------------------ src/logic/knowledge.js | 93 +++++++++++++++-------------- src/logic/time.js | 4 +- src/main.js | 2 +- src/utilities.js | 30 +++++++++- src/view/grid.js | 7 +++ src/view/timeController.js | 4 +- 9 files changed, 212 insertions(+), 128 deletions(-) create mode 100644 src/logic/arrangment.js diff --git a/src/index.html b/src/index.html index cd9f312..8def0ba 100644 --- a/src/index.html +++ b/src/index.html @@ -21,6 +21,7 @@ + diff --git a/src/logic/arrangment.js b/src/logic/arrangment.js new file mode 100644 index 0000000..3a91a8d --- /dev/null +++ b/src/logic/arrangment.js @@ -0,0 +1,116 @@ +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.clear() + } + + constructor(products = []) { + this.products = products; + this.clear() + } + + clear() { + this.productsAmount = null; + this.productsAttributes = null; + this.similarities = null; + this.cachedValidity = null; + this.similarity = null; + } + + clone() { + return JSON.parse(JSON.stringify(this.products)); + } + + simulate() { + this.similarities ||= [...Array(ShelfCount)].map(() => 0); + this.productsAmount ||= [...Array(ShelfCount)].map(() => 50); + this.productsAttributes ||= this.products.map(name => Knowledge.productAttributes.getRandomFor(name)); + + 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; + } + + async validate() { + if (this.cachedValidity) + return this.cachedValidity; + + const matchingShelfs = this.products.map(async (product, i) => { + if (!this.productsAttributes[i]) + return; + + let shelfType = await requestJSONCached('/api/decide', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + redirect: 'follow', + body: JSON.stringify(this.productsAttributes[i]) + }); + + if (!shelfType) + return; + shelfType = shelfType[0]; + return Grid.instance.getShelf(...i2gc(i)).type === shelfType; + }); + + let m = 0; + for (const match of matchingShelfs) + switch (await match) { + case undefined: + case true: + m++; + } + + return this.cachedValidity = m / this.productsAttributes.length; + } + + async fitness() { + if (this.similarity === null) this.simulate(); + if (this.cachedValidity === null) await this.validate(); + return (this.cachedValidity + this.similarity) / 2; + } +} diff --git a/src/logic/evolution.js b/src/logic/evolution.js index 55f8fb3..c850a9d 100644 --- a/src/logic/evolution.js +++ b/src/logic/evolution.js @@ -1,75 +1,3 @@ -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; @@ -98,14 +26,15 @@ class Evolution extends Arrangement { this.max_similarity = this.population.reduce((p, c) => Math.max(p, c.similarity || 0), 0); } - next(days) { + async next(days) { for (let i = 0; i < Number(days) - 1; ++i) { - this.step(false); + await this.step(false); } - this.step(true); + await this.step(true); } - step(render = true) { + + async step(render = true) { // Complete previous day this.products = this.population[this.populationIndex].products; this.simulate(); @@ -124,7 +53,7 @@ class Evolution extends Arrangement { let newPopulation = []; this.population.sort((a, b) => a.similarity - b.similarity); - let fitness = this.population.map(({ similarity }) => similarity); + let fitness = await Promise.all(this.population.map(arr => arr.fitness())); let sum = fitness.reduce((p, c) => p + c, 0); fitness = fitness.map(f => Math.pow(f / sum, 2)); diff --git a/src/logic/knowledge.js b/src/logic/knowledge.js index b53eeae..18b6b91 100644 --- a/src/logic/knowledge.js +++ b/src/logic/knowledge.js @@ -52,6 +52,31 @@ class SemanticNetwork { } } +class AttributesSemanticNetwork { + constructor(data) { + this.entries = {} + for (const { nazwa, polka, ...attr } of data) { + if (!this.entries[nazwa]) { + this.entries[nazwa] = Object.entries(attr).reduce((p, [key, value]) => ({ ...p, [key]: new Set([value]) }), {}); + continue; + } + + for (const [key, value] of Object.entries(attr)) { + this.entries[nazwa][key].add(value); + } + } + } + + getRandomFor(productName) { + const attributes = this.entries[productName]; + return attributes && Object.entries(attributes) + .reduce((p, [key, values]) => { + const r = randomFromSet(values); + return r !== 'nie_dotyczy' ? { ...p, [key]: r } : p; + }, {}); + } +} + function nearbyStorageUnitsCoords(gridX, gridY) { function outsideOfStorageCenter(v) { const { x, y, w, h } = StorageCenterLocation @@ -73,6 +98,22 @@ function nearbyStorageUnitsIndexes(gridX, gridY) { } +async function loadTsvFile(path, nested = []) { + const response = await fetch(path); + const text = await response.text(); + + const [header, ...rows] = text.split('\n') + .map(x => x.trim()) + .filter(x => x) + .map(x => x.split('\t') + .map(cell => cell.split(',').map(x => x.trim()))); + + return rows.map(row => + row.reduce((p, c, i) => ({ + ...p, + [header[i]]: nested.includes(i) ? c : c[0] }), {})); +} + class Knowledge { /** @@ -80,51 +121,13 @@ class Knowledge { */ static semanticNetwork - static async loadFromFile(path) { - const response = await fetch(path); - const text = await response.text(); - - const [header, ...rows] = text.split('\n') - .map(x => x.trim()) - .filter(x => x) - .map(x => x.split('\t') - .map(cell => cell.split(',').map(x => x.trim()))); + static async load() { + const [ products, attributes ] = await Promise.all([ + loadTsvFile('/data/products.tsv', [ 2 ]), + loadTsvFile('/data/productsTree.tsv') + ]); - const data = rows.map(row => - row.reduce((p, c, i) => ({ - ...p, - [header[i]]: i == 2 ? c : c[0] }), {})); - - Knowledge.semanticNetwork = new SemanticNetwork(data); - } - - constructor() { - Knowledge.semanticNetwork = new SemanticNetwork(definition) - Knowledge.arrangement = new Arrangement() + Knowledge.semanticNetwork = new SemanticNetwork(products); + Knowledge.productAttributes = new AttributesSemanticNetwork(attributes); } } - - - -/* -keywords: - p - powiązane pomieszczenia / miejsca - c - category - -new Knowledge(` - Piłka :c sport,rozrywka, piłka nożna - Kubek klubowy :c sport,rozrywka,piłka nożna :p kuchnia - Waga łazienkowa :c zdrowie,agd,elektronika :p łazienka - Czajnik elektryczny :c agd,elektronika,zdrowie :p kuchnia - Widelec :c agd,jedzenie,zastawa kuchenna :p kuchnia - Łyżka :c agd,jedzenie,zastawa kuchenna :p kuchnia - Nóż stołowy :c agd,jedzenie,zastawa kuchenna :p kuchnia - Nóż kuchenny :c agd,gotowanie,przyrząd kuchenny :p kuchnia - Deska do krojenia :cagd,gotowanie,przyrząd kuchenny :p kuchnia - Gąbki kuchenne :c agd,gotowanie,sprzątanie :p kuchnia - Worki na śmieci :c agd,gotowanie,sprzątanie :p kuchnia - Miska :c agd,jedzenie,gotowanie,zastawa kuchenna :p kuchnia - Laptop :c elektronika,gaming :p biuro,szkoła - Telefon :c elektronika :p biuro,szkoła -`) -*/ \ No newline at end of file diff --git a/src/logic/time.js b/src/logic/time.js index 762b696..263baf2 100644 --- a/src/logic/time.js +++ b/src/logic/time.js @@ -57,8 +57,8 @@ class Time { /** * Add days */ - addDays(days) { - Products.instance.next(days); + async addDays(days) { + await Products.instance.next(days); Products.instance.update(); const ul = document.querySelector('ul#history'); this.setDay(this.day += Number(days)); diff --git a/src/main.js b/src/main.js index 1326ab4..fa24462 100644 --- a/src/main.js +++ b/src/main.js @@ -14,7 +14,7 @@ window.addEventListener('DOMContentLoaded', async () => { const grid = new Grid(gridCanvas); const agent = new Agent(agentCanvas); - await Knowledge.loadFromFile('/data/products.tsv') + await Knowledge.load() const products = new Products(productsCanvas); const fpsElement = document.getElementById('fps'); diff --git a/src/utilities.js b/src/utilities.js index 614b4d2..9aa390e 100644 --- a/src/utilities.js +++ b/src/utilities.js @@ -50,4 +50,32 @@ function pick(array) { return index - 1; } -function nice(v) { return `${(v * 100).toFixed(1)}%` } \ No newline at end of file +function unreachable() { + throw new Error('unreachable'); +} + +function randomFromSet(set) { + const r = Math.floor(Math.random() * set.size); + let i = 0; + for (const value of set.values()) { + if (i++ === r) + return value; + } + unreachable(); +} + + +function nice(v) { return `${(v * 100).toFixed(1)}%` } + +async function requestJSONCached(url, params = {}) { + const key = url + '\n' + (params?.body || ''); + if (requestJSONCached.cache.has(key)) + return requestJSONCached.cache.get(key); + + const response = await fetch(url, params); + const json = await response.json(); + requestJSONCached.cache.set(key, json); + return json; +} + +requestJSONCached.cache = new Map(); \ No newline at end of file diff --git a/src/view/grid.js b/src/view/grid.js index 768fba7..f90b7cd 100644 --- a/src/view/grid.js +++ b/src/view/grid.js @@ -5,6 +5,9 @@ const GRID_FIELD_TYPE = Object.freeze({ }); class Grid { + /** + * @type{Grid} + */ static instance; constructor(canvas) { @@ -31,6 +34,10 @@ class Grid { return this.shelves; } + getShelf(x, y) { + return this.shelves[x][y]; + } + setCanvasSize (canvas) { canvas.width = 2000; canvas.height = 900; diff --git a/src/view/timeController.js b/src/view/timeController.js index e9655bb..4c39cfd 100644 --- a/src/view/timeController.js +++ b/src/view/timeController.js @@ -13,7 +13,7 @@ class TimeController { } } - readFromDayInput() { - Time.instance.addDays(document.getElementById('dayInput').value); + async readFromDayInput() { + await Time.instance.addDays(document.getElementById('dayInput').value); } } \ No newline at end of file