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