diff --git a/action.js b/action.js new file mode 100644 index 0000000..3fe7bf2 --- /dev/null +++ b/action.js @@ -0,0 +1,45 @@ +const EMPTY = "empty" +const FILL = "fill" +const POUR = "pour" +const UNKNOWN = "unknown" + +class Action { + constructor({ kind = UNKNOWN, target, src } = {}) { + this.kind = kind + this.target = target + this.src = src + } + + isEmpty() { + return this.kind === EMPTY; + } + + isFill() { + return this.kind === FILL; + } + + isPour() { + return this.kind === POUR; + } + + string() { + if (this.kind === POUR) { + return `${this.kind} ${this.src} → ${this.target}` + } + return `${this.kind} ${this.target}` + } +} + +module.exports = { + fill(index) { + return new Action({ kind: FILL, target: index }) + }, + + empty(index) { + return new Action({ kind: EMPTY, target: index }) + }, + + pour(src, target) { + return new Action({ kind: POUR, target, src }) + }, +} diff --git a/bucket.js b/bucket.js new file mode 100644 index 0000000..acb8fb3 --- /dev/null +++ b/bucket.js @@ -0,0 +1,39 @@ +class Bucket { + constructor({ capacity, volume = 0 }) { + this.capacity = capacity + this.volume = volume + } + + fill() { + this.volume = this.capacity + } + + empty() { + this.volume = 0 + } + + isEmpty() { + return this.volume === 0 + } + + isFull() { + return this.volume == this.capacity + } + + pour(bucket) { + const availableVolume = bucket.capacity - bucket.volume + if (availableVolume > this.volume) { + bucket.volume += this.volume + this.volume = 0 + } else { + this.volume -= availableVolume + bucket.volume += availableVolume + } + } + + string() { + return `${this.volume}vol/${this.capacity}cap` + } +} + +module.exports = Bucket; diff --git a/buckets.js b/buckets.js new file mode 100644 index 0000000..1d2073a --- /dev/null +++ b/buckets.js @@ -0,0 +1,134 @@ +const Bucket = require('./bucket'); +const { fill, empty, pour } = require('./action'); + +class Buckets { + constructor(buckets = []) { + this.buckets = buckets; + } + + hasTarget(target) { + return !!this.buckets.find(bucket => bucket.volume === target) + } + + performAction(action) { + if (action.isEmpty()) { + this.buckets[action.target].empty() + } + if (action.isFill()) { + this.buckets[action.target].fill() + } + if (action.isPour()) { + this.buckets[action.src].pour(this.buckets[action.target]) + } + } + + possibleActions() { + const ret = [] + this.buckets.forEach((bucket, index) => { + if (!bucket.isFull()) { + ret.push(fill(index)); + } + if (!bucket.isEmpty()) { + ret.push(empty(index)); + this.buckets.forEach((dest, j) => { + if (index === j) { + return + } + if (!dest.isFull()) { + ret.push(pour(index, j)); + } + }) + } + }) + return ret + } + + string() { + return this.buckets.map(bucket => bucket.string()).join(', ') + } + + findTarget(target) { + const results = { + mapping: { [this.string()]: [[]] }, + matching: new Set(), + } + this._findTarget(target, results, [], 0) + + let best = null + + results.matching.forEach(matchingKey => { + const result = results.mapping[matchingKey] + if (best === null) { + best = result; + return + } + if (best.length == result.length) { + best.push(result); + } + if (best.length > result.length) { + best = result; + } + }) + + return best || [] + } + + _findTarget(target, state, actions = [], depth = 0) { + if (this.hasTarget(target)) { + return + } + const possibleActions = this.possibleActions(); + const results = possibleActions.map(action => { + const copy = fromJSON(this); + copy.performAction(action); + return copy; + }); + + const numberOfKeys = Object.keys(state.mapping).length + + results.forEach(((result, i) => { + const key = result.string(); + const existingActions = state.mapping[key]; + const action = possibleActions[i]; + + if (result.hasTarget(target)) { + state.matching.add(result.string()); + } + + if (!existingActions) { + state.mapping[key] = [[...actions, action]] + return + } + + if (existingActions[0].length == actions.length + 1) { + state.mapping[key].push([...actions, possibleActions[i]]) + return + } + + if (existingActions[0].length > actions.length + 1) { + state.mapping[key] = [[...actions, action]] + } + })); + + if (Object.keys(state.mapping).length === numberOfKeys) { + return + } + + results.forEach(((result, i) => { + const action = possibleActions[i]; + result._findTarget(target, state, [...actions, action], depth + 1) + })) + } +} + +function fromJSON(json) { + const jsonStr = JSON.stringify(json) + const newJSON = JSON.parse(jsonStr) + const buckets = newJSON.buckets.map(val => new Bucket(val)); + return new Buckets(buckets) +} + +module.exports = { + fromJSON, + Buckets, +} diff --git a/main.js b/main.js new file mode 100644 index 0000000..3fea5cb --- /dev/null +++ b/main.js @@ -0,0 +1,56 @@ +#!/usr/bin/env node + +const { fromJSON } = require('./buckets') + +const usage = `main.js target bucket [bucket...] + +target: volume of bucket to search for +bucket: volume of bucket +` + +function bail() { + console.log(usage); + process.exit(1) +} + +const args = [...process.argv].slice(2) + +if (args.length < 2) { + bail(); +} + +const intArgs = args.map(arg => Number.parseInt(arg, 10)) + +const hasNaN = intArgs.find(arg => Number.isNaN(arg)) +if (hasNaN) { + bail(); +} + +const target = intArgs[0]; + +const bucketsJSON = intArgs.slice(1).map(capacity => ({ capacity })) + +const buckets = fromJSON({ buckets: bucketsJSON }) + +const result = buckets.findTarget(target) + +if (result.length === 0) { + console.log('No results') +} + +if (result.length === 1) { + console.log('1 solution') +} + +if (result.length > 1) { + console.log(`${result.length} solutions`) +} + +result.forEach((actions, i) => { + console.log(`solution ${i + 1}`) + const copy = fromJSON(buckets); + actions.forEach(action => { + copy.performAction(action) + console.log(`${action.string()} resulting in ${copy.string()}`) + }) +});