diff --git a/apps/TOH/Driver.cpp b/apps/TOH/Driver.cpp index 4ab788a4d..c16cd4fde 100644 --- a/apps/TOH/Driver.cpp +++ b/apps/TOH/Driver.cpp @@ -8,13 +8,14 @@ #include "TemplateAStar.h" #include "FileUtil.h" #include "SVGUtil.h" -const int numDisks = 16; +const int numDisks = 12; void SaveSVG(Graphics::Display &d, int port); TOH toh; TOHState s, g; + IDAStar, TOHMove> ida; std::vector solution; @@ -43,8 +44,19 @@ TOHPDB pdb4(&absToh1, goal); Heuristic> h; +int selectedPeg = -1; bool recording = false; -bool running = true; +bool animating = false; +bool dragging = false; +bool releasing = false; +float px; // cursor's x-position +int userMoveCount = 0; +int optimalMoveCount = 0; +float userSolveTime = 0; +float optimalSolveTime = 0; +std::string str; +Timer t; + int main(int argc, char* argv[]) { @@ -62,7 +74,8 @@ void InstallHandlers() InstallKeyboardHandler(MyDisplayHandler, "PDB", "Value-Range Compress PDB", kAnyModifier, 'v'); InstallKeyboardHandler(MyDisplayHandler, "Record", "Record a movie", kAnyModifier, 'r'); InstallKeyboardHandler(MyDisplayHandler, "Reset View", "Reset camera to initial view", kAnyModifier, '|'); - InstallKeyboardHandler(MyDisplayHandler, "Reset state", "Choose a new random initial state", kAnyModifier, 's'); + InstallKeyboardHandler(MyDisplayHandler, "Solve", "Find optimal solution path", kAnyModifier, 's'); + InstallKeyboardHandler(MyDisplayHandler, "Reset", "Reset the board", kAnyModifier, 'n'); InstallCommandLineHandler(MyCLHandler, "-run", "-run", "Runs pre-set experiments."); @@ -73,11 +86,11 @@ void InstallHandlers() void SolveProblem(bool reset = true) { - Timer t; + if (reset) { - s.StandardStart(); - g.Reset(); + s.StandardStart(); //the start state + g.Reset(); //the solution state // g.counts[2]++; // g.counts[3]--; // g.disks[2][0] = 4; @@ -123,12 +136,17 @@ void SolveProblem(bool reset = true) t.StartTimer(); astar.GetPath(&toh, s, g, solution); t.EndTimer(); + + optimalSolveTime = t.GetElapsedTime(); + optimalMoveCount = solution.size(); + for (auto &m : solution) { std::cout << m << " "; } - printf("\n[no prune] %1.2fs elapsed; %llu expanded; %llu generated [%lu]\n", t.GetElapsedTime(), astar.GetNodesExpanded(), astar.GetNodesTouched(), solution.size()); - + printf("\n[no prune] %1.2fs elapsed; %llu expanded; %llu generated [%lu]\n", optimalSolveTime, astar.GetNodesExpanded(), astar.GetNodesTouched(), optimalMoveCount); + + } void SpeedTest() @@ -176,48 +194,104 @@ void MyWindowHandler(unsigned long windowID, tWindowEventType eType) h.heuristics.push_back(&toh); h.lookups.push_back({kLeafNode, 0, 0}); s.StandardStart(); - g.Reset(); + g.StandardStart(); + setTextBufferVisibility(false); // SolveProblem(true); // SpeedTest(); } } -double v = 1; -uint64_t counter = 0; -const int animationFrames = 20; +float v = 0.0; +const int animationFrames = 15; void MyFrameHandler(unsigned long windowID, unsigned int viewport, void *) { // cameraMoveTo(0, -1, -12.5, 0.05); // cameraLookAt(0, 0, 0, 0.2); auto &display = GetContext(windowID)->display; - display.FillRect({-1, -1, 1, 1}, Colors::black); - toh.Draw(display); - - if (solution.size() != 0 && running) - { - toh.GetNextState(s, solution[0], g); - toh.Draw(display, s); - SaveSVG(display, 0); -// toh.OpenGLDraw(s, g, float(counter)/animationFrames); -// counter = (counter+1)%animationFrames; -// if (counter == 0) - { - toh.ApplyAction(s, solution[0]); - solution.erase(solution.begin()); -// if (solution.size() == 0) -// counter = 10;//recording = false; - } - } + display.FillRect({-1, -1, 1, 1}, Colors::white); // background + + toh.Draw(display); // pegs, base + toh.Draw(display, str); // text area + + if (animating) + { // computer is solving + if (v == 0) + { + s = g; + toh.GetNextState(s, solution[0], g); // g is the next state + solution.erase(solution.begin()); + } + + toh.Draw(display, s, g, v); + + if (solution.size() == 0 && v >= 1 - 1.0/animationFrames) + { // game is solved + s = g; + animating = false; + str = "Optimal solve is "+std::to_string(optimalMoveCount)+" moves in "+std::to_string(optimalSolveTime)+" seconds"; + } + } + else if (dragging) + { // user is playing, dragging a disk + if (v < 0.333) // disk animates up + { + toh.Draw(display, s, selectedPeg, 0, v); // disk on selectedPeg starts going to peg 0 (it doesn't matter if peg 0 is valid because only the first third of the move is shown) + } + else { + v -= 1.0/animationFrames; // to offset v += 1.0/animationFrames, so the end result is v doesn't change + g = s; + toh.Draw(display, s, selectedPeg, px); + } + + } + else if (releasing) + { // user is playing, releasing a disk + int nextPeg = toh.GetHoveredPeg(px); + toh.Draw(display, s, selectedPeg, nextPeg, v); + + if (v >= 1 - 1.0/animationFrames) // once we're all the way through v, reset everything + { + releasing = false; + s = g; // the old next state is now the current state + selectedPeg = -1; + + if (userSolveTime == 1) + { + t.EndTimer(); + userSolveTime = t.GetElapsedTime(); + } + + if (userSolveTime == 0) + { + t.StartTimer(); + userSolveTime++; // to prevent the timer from restarting + } + if (s.GetDiskCountOnPeg(3) == numDisks) + { // game is solved + t.EndTimer(); + userSolveTime = t.GetElapsedTime(); + userSolveTime = std::round(userSolveTime*10); + int timeDec = static_cast(userSolveTime) % 10; // tenth place digit of userSolveTime rounded to the nearest tenth + int timeInt = (static_cast(userSolveTime) - timeDec) / 10; // integer part of userSolveTime + + str = "Congrats! You solved with "+std::to_string(userMoveCount)+" moves in "+std::to_string(timeInt)+"."+std::to_string(timeDec)+" seconds"; + } + else { + str = "moves taken: "+std::to_string(userMoveCount); + } + } + } else { -// if (solution.size() == 0) -// { -// counter--; -// if (counter == 0) -// recording = false; -// } toh.Draw(display, s); } + + v += 1.0/animationFrames; + + if (v >= 1) + v = 0; + + if (recording && viewport == GetNumPorts(windowID)-1) { // static int cnt = 0; @@ -244,17 +318,22 @@ void MyDisplayHandler(unsigned long windowID, tKeyboardModifier mod, char key) case 'r': recording = !recording; break; case 's': { - //toh.GetStateFromHash(random()%toh.GetNumStates(s), s); -// g.Reset(); SolveProblem(true); -// ida.GetPath(&toh, s, g, solution); -// for (auto &m : solution) -// { -// std::cout << m << " "; -// } -// std::cout << "\n"; + g = s; + v = 0; + str = ""; + animating = true; } break; + case 'n': + s.StandardStart(); + g.StandardStart(); + userMoveCount = 0; + userSolveTime = 0; + animating = false; + releasing = false; + str = ""; + break; case 'v': // value range compression { goal.Reset(); @@ -375,12 +454,36 @@ Heuristic> *BuildPDB(const TOHState &goal) return h; } -bool MyClickHandler(unsigned long , int, int, point3d , tButtonType , tMouseEventType ) +bool MyClickHandler(unsigned long , int, int, point3d p, tButtonType , tMouseEventType e) { - return false; + if (animating) + return true; + if (s.GetDiskCountOnPeg(3) == numDisks) // stops letting the user move disks after the game has been solved + return true; + + if (e == kMouseDown) + { + v = 0; + releasing = false; + s = g; + toh.Click(selectedPeg, p.x); + } + else if (e == kMouseDrag) + { + px = p.x; + dragging = toh.Drag(s, selectedPeg); + } + else if (e == kMouseUp) + { + dragging = false; + releasing = toh.Release(s, selectedPeg, p, g, userMoveCount); + + v = 0.667; + } + + return true; } - void SaveSVG(Graphics::Display &d, int port) { const std::string baseFileName = "/Users/nathanst/Pictures/hog2/TOH_"; diff --git a/environments/TOH.h b/environments/TOH.h index 6da64e74e..8c8e457e8 100644 --- a/environments/TOH.h +++ b/environments/TOH.h @@ -158,11 +158,18 @@ class TOH : public SearchEnvironment, TOHMove> { void OpenGLDraw(const TOHState&, const TOHState&, float) const; void OpenGLDraw(const TOHState&, const TOHMove&) const; - void Draw(Graphics::Display &display) const; - void Draw(Graphics::Display &display, const TOHState &s) const; - void Draw(Graphics::Display &display, const TOHState&, TOHMove&) const; - void Draw(Graphics::Display &display, const TOHState &l1, const TOHState &l2, float v) const; - + void Draw(Graphics::Display &display, std::string str) const; // draws text + void Draw(Graphics::Display &display) const; // draws the base and lines + void Draw(Graphics::Display &display, const TOHState &s) const; // draws the disks when not animating + void Draw(Graphics::Display &display, const TOHState &l1, const TOHState &l2, float v) const; // animation for optimal solution + void Draw(Graphics::Display &display, const TOHState &l1, int selectedPeg, int nextPeg, float v) const; // vertical animation for when user is solving + void Draw(Graphics::Display &display, const TOHState &l1, int startPeg, float px); // horizontal animation for when user is solving + void Draw(Graphics::Display &display, const TOHState&, TOHMove&) const; + + bool Click(int &peg, float px); + int GetHoveredPeg(const float &px); + bool Drag(const TOHState &currState, int peg); + bool Release(const TOHState &currState, int &peg, point3d loc, TOHState &nextState, int &userMoveCount); bool pruneActions; protected: @@ -425,97 +432,196 @@ void TOH::OpenGLDraw(const TOHState&, const TOHMove&) const } + +// Draw for text area +template +void TOH::Draw(Graphics::Display &display, std::string str) const +{ + Graphics::rect r1(-1, -1, 1, -0.8); // background for text area + display.FillRect(r1, Colors::lightgray); + + display.DrawText(str.c_str(), Graphics::point{-0.9, -0.9}, Colors::black, 0.075, + Graphics::textAlignLeft, Graphics::textBaselineMiddle); +} + +// Draw for pegs and base template void TOH::Draw(Graphics::Display &display) const { - Graphics::rect r1(-0.75-0.01, -0.8, -0.75+0.01, 0); - display.FillRect(r1, Colors::lightgray); - r1.left += 0.5; r1.right += 0.5; - display.FillRect(r1, Colors::lightgray); + Graphics::rect r1(-0.75-0.01, 0, -0.75+0.01, 0.9); // peg + + display.FillRect(r1, Colors::gray); + + r1.left += 0.5; r1.right += 0.5; // adds margin of space to the left and right + display.FillRect(r1, Colors::gray); r1.left += 0.5; r1.right += 0.5; - display.FillRect(r1, Colors::lightgray); + display.FillRect(r1, Colors::gray); r1.left += 0.5; r1.right += 0.5; - display.FillRect(r1, Colors::lightgray); + display.FillRect(r1, Colors::gray); r1.left += 0.5; r1.right += 0.5; - display.FillRect({-1, 0, 1, 0.12}, {0.6, 0.4, 0.2}); -// glColor3f(0.5, 0.5, 0.5); -// DrawCylinder(-0.75, 0, 0, 0, 0.01, 0.8); -// DrawCylinder(-0.25, 0, 0, 0, 0.01, 0.8); -// DrawCylinder( 0.25, 0, 0, 0, 0.01, 0.8); -// DrawCylinder( 0.75, 0, 0, 0, 0.01, 0.8); -// glColor3f(0.6, 0.4, 0.2); -// glPushMatrix(); -// glScalef(1.0, 0.05, 0.25); -// DrawBox(0, 0.4/0.05+1, 0, 1.0); -// glPopMatrix(); - // DrawBox(0, 0, 0, 0.5); + + display.FillRect({-1, 0.8, 1, 0.92}, {0.6, 0.4, 0.2}); // brown base } +// Draw for still state template void TOH::Draw(Graphics::Display &display, const TOHState &s) const { - glColor3f(0.0, 0.0, 1.0); double offset[4] = {-0.75, -0.25, 0.25, 0.75}; for (int x = 0; x < 4; x++) { for (int y = 0; y < s.GetDiskCountOnPeg(x); y++) { int which = s.GetDiskOnPeg(x, y); - //glColor3f(0.0, 1.0-float(which)/float(disks), 1.0); rgbColor color(0.0, 1.0-float(which)/float(disks), 1.0); Graphics::rect r(offset[x]-0.04-0.2*which/float(disks), - 0-0.4/(1+float(disks))-y*0.8/(1+float(disks))-0.8/(1+float(disks)), + -y*0.8/(1+float(disks))-0.8/(1+float(disks))+0.8, offset[x]+0.04+0.2*which/float(disks), - 0-0.4/(1+float(disks))-y*0.8/(1+float(disks))); -// DrawCylinder(offset[x], 0.4-0.4/(1+float(disks))-y*0.8/(1+float(disks)), 0, -// 0.02, 0.04+0.2*which/float(disks), 0.8/(1+float(disks))); + -y*0.8/(1+float(disks))+0.8); display.FillRect(r, color); } } } +// Draw for animating optimal solve template -void TOH::Draw(Graphics::Display &display, const TOHState &s, const TOHState &s2, float interval) const +void TOH::Draw(Graphics::Display &display, const TOHState &s, const TOHState &s2, float v) const { - TOHMove m = this->GetAction(s, s2); - int animatingDisk = s.GetSmallestDiskOnPeg(m.source); - int initialHeight = s.GetDiskCountOnPeg(m.source)-1; - int finalHeight = s.GetDiskCountOnPeg(m.dest); - - glColor3f(0.0, 0.0, 1.0); - double offset[4] = {-0.75, -0.25, 0.25, 0.75}; - for (int x = 0; x < 4; x++) - { - for (int y = 0; y < s.GetDiskCountOnPeg(x); y++) - { - int which = s.GetDiskOnPeg(x, y); - if (which != animatingDisk) - { - glColor3f(0.0, 1.0-float(which)/float(disks), 1.0); - DrawCylinder(offset[x], 0.4-0.4/(1+float(disks))-y*0.8/(1+float(disks)), 0, - 0.02, 0.04+0.2*which/float(disks), 0.8/(1+float(disks))); - } - } - } - glColor3f(0.0, 1.0-float(animatingDisk)/float(disks), 1.0); - if (interval <= 0.333) - { - interval *= 3; - DrawCylinder(offset[m.source], 0.4-0.4/(1+float(disks))-initialHeight*0.8/(1+float(disks)) - (interval)*(disks+1-initialHeight)*0.8/(1+float(disks)), 0, - 0.02, 0.04+0.2*animatingDisk/float(disks), 0.8/(1+float(disks))); - } - else if (interval <= 0.666) - { - interval *= 3; - DrawCylinder((2-interval)*offset[m.source]+(interval-1)*offset[m.dest], 0.4-0.4/(1+float(disks))-0.8-0.2*sin((interval-1)*PI), 0, - 0.02, 0.04+0.2*animatingDisk/float(disks), 0.8/(1+float(disks))); - } - else { - DrawCylinder(offset[m.dest], 0.4-0.4/(1+float(disks))-finalHeight*0.8/(1+float(disks)) - - ((1.0-interval)/0.334)*(disks+1-finalHeight)*0.8/(1+float(disks)), 0, - 0.02, 0.04+0.2*animatingDisk/float(disks), 0.8/(1+float(disks))); - }} + TOHMove m = this->GetAction(s, s2); + + int animatingDisk = s.GetSmallestDiskOnPeg(m.source); + int finalHeight = s.GetDiskCountOnPeg(m.dest); + + float offset[4] = {-0.75, -0.25, 0.25, 0.75}; // x-positions of the pegs + for (int x = 0; x < 4; x++) + { + for (int y = 0; y < s.GetDiskCountOnPeg(x); y++) + { + int which = s.GetDiskOnPeg(x, y); + float halfwidth = 0.04+0.2*which/float(disks); + if (which != animatingDisk) // first, draws every disk except for the animating one + { + display.FillRect({static_cast(offset[x]-halfwidth), static_cast(0.8-0.8/(1+float(disks))-y*0.8/(1+float(disks))), static_cast(offset[x]+halfwidth), static_cast(0.8-y*0.8/(1+float(disks)))}, {0.0, static_cast(1.0-float(which)/float(disks)), 1.0}); + } + else { + int targetPeg = m.dest; + Graphics::rect r1; + Graphics::rect r2; + + if (v <= 0.333) { // up + v *= 3; + r1 = {static_cast(offset[x]-halfwidth), static_cast(0.8-0.8/(1+float(disks))-y*0.8/(1+float(disks))), static_cast(offset[x]+halfwidth), static_cast(0.8-y*0.8/(1+float(disks)))}; + + r2 = {static_cast(offset[x]-halfwidth), static_cast(-0.5-0.8/(1+float(disks))), static_cast(offset[x]+halfwidth), static_cast(-0.5)}; + } + else if (v <= 0.666) { // horizontal + v = (v - 0.333) * 3; + r1 = {static_cast(offset[x]-halfwidth), static_cast(-0.5-0.8/(1+float(disks))), static_cast(offset[x]+halfwidth), static_cast(-0.5)}; + + r2 = {static_cast(offset[targetPeg]-halfwidth), static_cast(-0.5-0.8/(1+float(disks))), static_cast(offset[targetPeg]+halfwidth), static_cast(-0.5)}; + } + else { // down + v = (v - 0.666) * 3; + r1 = {static_cast(offset[targetPeg]-halfwidth), static_cast(-0.5-0.8/(1+float(disks))), static_cast(offset[targetPeg]+halfwidth), static_cast(-0.5)}; + + r2 = {static_cast(offset[targetPeg]-halfwidth), static_cast(0.8-0.8/(1+float(disks))-finalHeight*0.8/(1+float(disks))), static_cast(offset[targetPeg]+halfwidth), static_cast(0.8-finalHeight*0.8/(1+float(disks)))}; + } + + r1.lerp(r2, v); + display.FillRect(r1, Colors::purple); + } + } + } + + +} + +// Draw for vertical animation when user is solving +template +void TOH::Draw(Graphics::Display &display, const TOHState &s, int selectedPeg, int nextPeg, float v) const +{ + int animatingDisk = s.GetSmallestDiskOnPeg(selectedPeg); + int finalHeight = s.GetDiskCountOnPeg(nextPeg); + + float offset[4] = {-0.75, -0.25, 0.25, 0.75}; + for (int x = 0; x < 4; x++) + { + for (int y = 0; y < s.GetDiskCountOnPeg(x); y++) + { + int which = s.GetDiskOnPeg(x, y); + float halfwidth = 0.04+0.2*which/float(disks); + if (which != animatingDisk) // first, draws every disk except for the animating one + { + display.FillRect({static_cast(offset[x]-halfwidth), static_cast(0.8-0.8/(1+float(disks))-y*0.8/(1+float(disks))), static_cast(offset[x]+halfwidth), static_cast(0.8-y*0.8/(1+float(disks)))}, {0.0, static_cast(1.0-float(which)/float(disks)), 1.0}); + } + else { + Graphics::rect r1; + Graphics::rect r2; + + if (v <= 0.333) { // up for the first third + v *= 3; + r1 = {static_cast(offset[x]-halfwidth), static_cast(0.8-0.8/(1+float(disks))-y*0.8/(1+float(disks))), static_cast(offset[x]+halfwidth), static_cast(0.8-y*0.8/(1+float(disks)))}; + + r2 = {static_cast(offset[x]-halfwidth), static_cast(-0.5-0.8/(1+float(disks))), static_cast(offset[x]+halfwidth), static_cast(-0.5)}; + } + else { // down for the last third. the second third is animated by Draw(display, s, startPeg, px) + v = (v - 0.666) * 3; + r1 = {static_cast(offset[nextPeg]-halfwidth), static_cast(-0.5-0.8/(1+float(disks))), static_cast(offset[nextPeg]+halfwidth), static_cast(-0.5)}; + + r2 = {static_cast(offset[nextPeg]-halfwidth), static_cast(0.8-0.8/(1+float(disks))-finalHeight*0.8/(1+float(disks))), static_cast(offset[nextPeg]+halfwidth), static_cast(0.8-finalHeight*0.8/(1+float(disks)))}; + } + + r1.lerp(r2, v); + display.FillRect(r1, Colors::purple); + } + } + } + + +} + +// Draw for horizontal animation when user is solving +template +void TOH::Draw(Graphics::Display &display, const TOHState &s, int startPeg, float px) +{ + int animatingDisk = s.GetSmallestDiskOnPeg(startPeg); + + float offset[4] = {-0.75, -0.25, 0.25, 0.75}; // the x-positions for each peg + + // if the mouse is hovering over a peg, highlight that peg + int hoveredPeg = GetHoveredPeg(px); + if (hoveredPeg != -1) + { + Graphics::rect p(offset[hoveredPeg]-0.01, 0, offset[hoveredPeg]+0.01, 0.8); + + // highlight invalid pegs in red and valid pegs in purple + if (s.GetSmallestDiskOnPeg(startPeg) > s.GetSmallestDiskOnPeg(hoveredPeg)) + display.FillRect(p, Colors::red); + else + display.FillRect(p, Colors::purple); + + } + + for (int x = 0; x < 4; x++) + { + for (int y = 0; y < s.GetDiskCountOnPeg(x); y++) + { + int which = s.GetDiskOnPeg(x, y); + float halfwidth = 0.04+0.2*which/float(disks); + if (which != animatingDisk) // first, draws every disk except for the animating one + { + display.FillRect({static_cast(offset[x]-halfwidth), static_cast(0.8-0.8/(1+float(disks))-y*0.8/(1+float(disks))), static_cast(offset[x]+halfwidth), static_cast(0.8-y*0.8/(1+float(disks)))}, {0.0, static_cast(1.0-float(which)/float(disks)), 1.0}); + } + else + { // draws the animating disk + Graphics::rect r1 = {static_cast(px-halfwidth), static_cast(-0.5-0.8/(1+float(disks))), static_cast(px+halfwidth), -0.5f}; + display.FillRect(r1, Colors::purple); + } + } + } + +} template void TOH::Draw(Graphics::Display &display, const TOHState&, TOHMove&) const @@ -523,6 +629,78 @@ void TOH::Draw(Graphics::Display &display, const TOHState&, TOHMov // nothing here as in OpenGLDraw } +template +bool TOH::Click(int &peg, float px) +{ + peg = GetHoveredPeg(px); + + return true; +} + +template +int TOH::GetHoveredPeg(const float &px) +{ + int peg = -1; + + if (-0.75-0.1 <= px && px <= -0.75+0.1) // area accepted as "peg" goes a little beyond peg boundary + { + peg = 0; + } + else if (-0.25-0.1 <= px && px <= -0.25+0.1) + { + peg = 1; + } + else if (0.25-0.1 <= px && px <= 0.25+0.1) + { + peg = 2; + } + else if (0.75-0.1 <= px && px <= 0.75+0.1) + { + peg = 3; + } + + return peg; +} + + +template +bool TOH::Drag(const TOHState &currState, int peg) +{ + if (peg == -1) // if in empty space + return false; + + if (currState.GetDiskCountOnPeg(peg) == 0) // if the peg has no disks + return false; + + return true; +} + +template +bool TOH::Release(const TOHState &currState, int &peg, point3d loc, TOHState &nextState, int &userMoveCount) +{ + if (peg == -1) // no disk to release + return false; + + int nextPeg = GetHoveredPeg(loc.x); + + nextState = currState; + + if (peg == nextPeg) + return true; + + if (nextPeg != -1 && currState.GetSmallestDiskOnPeg(peg) < currState.GetSmallestDiskOnPeg(nextPeg)) // if the next peg is actually a valid next peg + { + TOHMove m = TOHMove(peg, nextPeg); + ApplyAction(nextState, m); + userMoveCount++; + + return true; + } + + return false; +} + + template class TOHPDB : public PDBHeuristic, TOHMove, TOH, TOHState> { public: