#import('dart:html');
final int CANVAS_WIDTH = 400;
final int CANVAS_HEIGHT = 300;
CanvasRenderingContext2D context = null;//Canvas 2d Context
final int CYLINDER_WIDTH = 75;
final int CYLINDER_BASE_WIDTH = 100;
final int CYLINDER_HEIGHT = 200;
int selectedValueA = 0;
int selectedValueB = 0;
int selectedValueG = 0;
InputElement sliderA, sliderB, sliderG;
InputElement valueA, valueB, valueG;
List bucketActions;
Bucket bucketA;
Bucket bucketB;
int maxLevel = 0;
int animationBucketActionStep = -1;
void main() {
CanvasElement canvas = query("#canvas");
context = canvas.context2d;
valueA = query("#valueA");
valueB = query("#valueB");
valueG = query("#valueG");
sliderA = query("#sliderA");
sliderA.on.change.add((Event e) {
selectedValueA = Math.parseInt(sliderA.value);
refreshTextInputs();
}, true);
selectedValueA = Math.parseInt(sliderA.value);
sliderB = query("#sliderB");
sliderB.on.change.add((Event e) {
selectedValueB = Math.parseInt(sliderB.value);
refreshTextInputs();
}, true);
selectedValueB = Math.parseInt(sliderB.value);
sliderG = query("#sliderG");
sliderG.on.change.add((Event e) {
selectedValueG = Math.parseInt(sliderG.value);
refreshTextInputs();
}, true);
selectedValueG = Math.parseInt(sliderG.value);
refreshTextInputs();
drawFrame(0, 0);
query("#run").on.click.add((Event e){
try{
startPuzzleCaluclation();
} catch(final Exception ex){
query("#output").innerHTML = "There was a problem: $ex";
}
}, true);
}
void refreshTextInputs(){
valueA.value = sliderA.value;
valueB.value = sliderB.value;
valueG.value = sliderG.value;
}
void startPuzzleCaluclation() {
assert(selectedValueG < selectedValueA || selectedValueG < selectedValueB);
assert(selectedValueA != selectedValueB);
if(selectedValueG > selectedValueA && selectedValueG > selectedValueB)
throw new IllegalArgumentException('The Goal cannot be greater than both Graduated Cylinders!');
else if(selectedValueA == selectedValueB && (selectedValueA != selectedValueG))
throw new IllegalArgumentException('Graduated Cylinders (Buckets) cannot be the same!');
else if(selectedValueA == 0 || selectedValueB == 0 || selectedValueG == 0)
throw new IllegalArgumentException('Values must be greater than zero!');
//TODO: more assertions needed: check non-zero, etc..
bucketActions = new List();
bucketA = new Bucket(selectedValueA, "A");
bucketB = new Bucket(selectedValueB, "B");
Bucket smallBucket = bucketA;
Bucket largeBucket = bucketB;
maxLevel = selectedValueB;
if(selectedValueA > selectedValueB) {
smallBucket = bucketB;
largeBucket = bucketA;
maxLevel = selectedValueA;
assert(selectedValueA % selectedValueB != 0);
if(selectedValueA % selectedValueB == 0 && (selectedValueA != selectedValueG || selectedValueB != selectedValueG))
throw new IllegalArgumentException('No solution because B is a multiple of A!');
} else {
assert(selectedValueB % selectedValueA != 0);
if(selectedValueB % selectedValueA == 0 && (selectedValueA != selectedValueG || selectedValueB != selectedValueG))
throw new IllegalArgumentException('No solution because A is a multiple of B!');
}
largeBucket.fill();
while(largeBucket.fillLevel != selectedValueG && smallBucket.fillLevel != selectedValueG) {
if(largeBucket.capacity == largeBucket.fillLevel)
largeBucket.transferTo(smallBucket);
else if(smallBucket.isFull())
smallBucket.empty();
else if(smallBucket.isEmpty())
largeBucket.transferTo(smallBucket);
else if(largeBucket.isEmpty())
largeBucket.fill();
else
largeBucket.transferTo(smallBucket);
}
//we have populated our state, now let's play an animation of the states
animationBucketActionStep = 0;
animateBucketAction();
}
void animateBucketAction(){
if(animationBucketActionStep >= bucketActions.length)
return;
BucketAction ba = bucketActions[animationBucketActionStep];
drawFrame(ba.fillLevelA, ba.fillLevelB);
displayBucketAction();
animationBucketActionStep++;
window.setTimeout(animateBucketAction, 2000);
}
void writeActionToLog(String action, Bucket a, Bucket b){
bucketActions.add(new BucketAction(action, a.fillLevel, b.fillLevel));
var sb = new StringBuffer();
for(var i = 0; i < bucketActions.length; i++){
sb.add("${bucketActions[i].action}: ${bucketActions[i].fillLevelA},${bucketActions[i].fillLevelB}
");
}
query("#output").innerHTML = sb.toString();
}
/**
* Draw the complete canvas
*/
void drawFrame(int bucketAFillLevel, int bucketBFillLevel) {
context.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
num paddingX = CYLINDER_BASE_WIDTH/2;
num paddingY = (CANVAS_HEIGHT - CYLINDER_HEIGHT)/2;
displayCylinder(paddingX, paddingY + CYLINDER_HEIGHT, "A", bucketA == null ? 0 : bucketA.capacity, bucketAFillLevel);
displayCylinder(CANVAS_WIDTH - paddingX - CYLINDER_BASE_WIDTH, paddingY + CYLINDER_HEIGHT, "B", bucketB == null ? 0 : bucketB.capacity, bucketBFillLevel);
}
/**
* Draw an unmarked Graduated Cylinder to the canvas.
*/
void displayCylinder(num xLoc, num yLoc, String bucketId, int bucketCapacity, int bucketFillLevel) {
num hbd = (CYLINDER_BASE_WIDTH - CYLINDER_WIDTH)/2;//half the difference between the base width and cylinder width
num heightRatio = 1.0;
if(maxLevel != 0) {
heightRatio = bucketCapacity / maxLevel;
//draw fill, base the pixels for each unit on the maxLevel and space available
context.fillStyle = '#369'; //a nice blue
num unitPixels = CYLINDER_HEIGHT / maxLevel;
for(int i = 0; i < bucketFillLevel; i++) {
context.fillRect(xLoc + hbd, yLoc - ((i + 1) * unitPixels), CYLINDER_WIDTH, unitPixels);
}
}
//draw cylinder outline to canvas
context.beginPath();
context.lineWidth = 2;
context.fillStyle = "#000";
context.strokeStyle = "#000";
context.moveTo(xLoc, yLoc);
context.lineTo(xLoc + CYLINDER_BASE_WIDTH, yLoc);
context.moveTo(xLoc + CYLINDER_WIDTH + hbd, yLoc);
context.lineTo(xLoc + CYLINDER_WIDTH + hbd, yLoc - (CYLINDER_HEIGHT * heightRatio));
context.moveTo(xLoc + hbd, yLoc - (CYLINDER_HEIGHT * heightRatio));
context.lineTo(xLoc + hbd, yLoc);
context.stroke();
context.closePath();
//draw the fill level
context.font = '30px sans-serif';
context.textBaseline = 'top';
context.textAlign = 'center';
context.fillText('$bucketFillLevel', xLoc + CYLINDER_BASE_WIDTH/2, yLoc);
//draw a cylinder label
context.fillStyle = "#008000";//a nice green
context.lineWidth = 2;
context.strokeStyle = "#0C0";
context.strokeText('$bucketId', xLoc + CYLINDER_BASE_WIDTH/2 + 1, yLoc - (CYLINDER_HEIGHT * heightRatio));
context.fillText('$bucketId', xLoc + CYLINDER_BASE_WIDTH/2, yLoc - (CYLINDER_HEIGHT * heightRatio));
}
/**
* Draw what just happened to the canvas
*/
void displayBucketAction() {
if(animationBucketActionStep < 0 || animationBucketActionStep >= bucketActions.length)
return;
//Draw Step: #
context.font = '16px sans-serif';
context.textBaseline = 'top';
context.textAlign = 'left';
context.fillText('Step: ${animationBucketActionStep + 1}', 10, 10);
BucketAction ba = bucketActions[animationBucketActionStep];
//Draw Action
context.fillStyle = "#000";
context.font = 'bolder 20px sans-serif';
context.textAlign = 'center';
context.fillText('${ba.action}', CANVAS_WIDTH/2, 10);
//Draw an indicator if we're done
if(animationBucketActionStep == bucketActions.length - 1) {
context.fillStyle = "#F00";
context.fillText('Done!', CANVAS_WIDTH/2, 50);
}
}
class BucketAction {
String action;
int fillLevelA;
int fillLevelB;
BucketAction( this.action, this.fillLevelA, this.fillLevelB);
}
class Bucket {
final int capacity;
int fillLevel;
String id;
Bucket(capacity, this.id) : capacity = capacity,
this.fillLevel = 0;
void fill(){
this.fillLevel = this.capacity;
writeActionToLog("Fill $id", bucketA, bucketB);
}
void empty(){
this.fillLevel = 0;
writeActionToLog("Empty $id", bucketA, bucketB);
}
void transferTo(Bucket t){
num amt = t.capacity - t.fillLevel;
if(amt > this.fillLevel)
amt = this.fillLevel;
assert(amt <= this.fillLevel);
this.fillLevel-=amt;
t.fillLevel+=amt;
writeActionToLog("Transfer $id to ${t.id}", bucketA, bucketB);
}
bool isFull() {
return this.capacity == this.fillLevel;
}
bool isEmpty() {
return this.fillLevel == 0;
}
}