From 0ee8d17ce47b20ab90bb96f20bcf4db37707b8ee Mon Sep 17 00:00:00 2001 From: HBB20 Date: Wed, 6 Sep 2017 20:00:06 -0500 Subject: [PATCH] Init Commit --- .DS_Store | Bin 0 -> 8196 bytes .gitignore | 149 ++++ AI_A1_Definition.docx | Bin 0 -> 210061 bytes VERSION | 1 + autograder.py | 358 ++++++++ commands.txt | 22 + eightpuzzle.py | 281 ++++++ game.py | 729 ++++++++++++++++ ghostAgents.py | 81 ++ grading.py | 323 +++++++ graphicsDisplay.py | 679 +++++++++++++++ graphicsUtils.py | 402 +++++++++ keyboardAgents.py | 84 ++ layout.py | 149 ++++ layouts/bigCorners.lay | 37 + layouts/bigMaze.lay | 37 + layouts/bigSafeSearch.lay | 8 + layouts/bigSearch.lay | 15 + layouts/boxSearch.lay | 14 + layouts/capsuleClassic.lay | 7 + layouts/contestClassic.lay | 9 + layouts/contoursMaze.lay | 11 + layouts/greedySearch.lay | 8 + layouts/mediumClassic.lay | 11 + layouts/mediumCorners.lay | 14 + layouts/mediumDottedMaze.lay | 18 + layouts/mediumMaze.lay | 18 + layouts/mediumSafeSearch.lay | 6 + layouts/mediumScaryMaze.lay | 18 + layouts/mediumSearch.lay | 8 + layouts/minimaxClassic.lay | 5 + layouts/oddSearch.lay | 7 + layouts/openClassic.lay | 9 + layouts/openMaze.lay | 23 + layouts/openSearch.lay | 7 + layouts/originalClassic.lay | 27 + layouts/powerClassic.lay | 7 + layouts/smallClassic.lay | 7 + layouts/smallMaze.lay | 10 + layouts/smallSafeSearch.lay | 15 + layouts/smallSearch.lay | 5 + layouts/testClassic.lay | 10 + layouts/testMaze.lay | 3 + layouts/testSearch.lay | 5 + layouts/tinyCorners.lay | 8 + layouts/tinyMaze.lay | 7 + layouts/tinySafeSearch.lay | 7 + layouts/tinySearch.lay | 7 + layouts/trappedClassic.lay | 5 + layouts/trickyClassic.lay | 13 + layouts/trickySearch.lay | 7 + pacman.py | 684 +++++++++++++++ pacmanAgents.py | 52 ++ projectParams.py | 18 + search.py | 119 +++ searchAgents.py | 542 ++++++++++++ searchTestClasses.py | 821 ++++++++++++++++++ submission_autograder.py | 41 + testClasses.py | 206 +++++ testParser.py | 85 ++ test_cases/CONFIG | 1 + test_cases/q1/CONFIG | 2 + test_cases/q1/graph_backtrack.solution | 7 + test_cases/q1/graph_backtrack.test | 32 + test_cases/q1/graph_bfs_vs_dfs.solution | 7 + test_cases/q1/graph_bfs_vs_dfs.test | 30 + test_cases/q1/graph_infinite.solution | 7 + test_cases/q1/graph_infinite.test | 30 + test_cases/q1/graph_manypaths.solution | 7 + test_cases/q1/graph_manypaths.test | 39 + test_cases/q1/pacman_1.solution | 40 + test_cases/q1/pacman_1.test | 27 + test_cases/q2/CONFIG | 2 + test_cases/q2/graph_backtrack.solution | 7 + test_cases/q2/graph_backtrack.test | 32 + test_cases/q2/graph_bfs_vs_dfs.solution | 7 + test_cases/q2/graph_bfs_vs_dfs.test | 30 + test_cases/q2/graph_infinite.solution | 7 + test_cases/q2/graph_infinite.test | 30 + test_cases/q2/graph_manypaths.solution | 7 + test_cases/q2/graph_manypaths.test | 39 + test_cases/q2/pacman_1.solution | 22 + test_cases/q2/pacman_1.test | 27 + test_cases/q3/CONFIG | 2 + test_cases/q3/graph_backtrack.solution | 7 + test_cases/q3/graph_backtrack.test | 32 + test_cases/q3/graph_bfs_vs_dfs.solution | 7 + test_cases/q3/graph_bfs_vs_dfs.test | 30 + test_cases/q3/graph_infinite.solution | 7 + test_cases/q3/graph_infinite.test | 30 + test_cases/q3/graph_manypaths.solution | 7 + test_cases/q3/graph_manypaths.test | 39 + test_cases/q3/ucs_0_graph.solution | 7 + test_cases/q3/ucs_0_graph.test | 39 + test_cases/q3/ucs_1_problemC.solution | 22 + test_cases/q3/ucs_1_problemC.test | 28 + test_cases/q3/ucs_2_problemE.solution | 22 + test_cases/q3/ucs_2_problemE.test | 28 + test_cases/q3/ucs_3_problemW.solution | 34 + test_cases/q3/ucs_3_problemW.test | 28 + test_cases/q3/ucs_4_testSearch.solution | 12 + test_cases/q3/ucs_4_testSearch.test | 16 + test_cases/q3/ucs_5_goalAtDequeue.solution | 7 + test_cases/q3/ucs_5_goalAtDequeue.test | 29 + test_cases/q4/CONFIG | 2 + test_cases/q4/astar_0.solution | 7 + test_cases/q4/astar_0.test | 39 + .../q4/astar_1_graph_heuristic.solution | 7 + test_cases/q4/astar_1_graph_heuristic.test | 54 ++ test_cases/q4/astar_2_manhattan.solution | 22 + test_cases/q4/astar_2_manhattan.test | 27 + test_cases/q4/astar_3_goalAtDequeue.solution | 7 + test_cases/q4/astar_3_goalAtDequeue.test | 29 + test_cases/q4/graph_backtrack.solution | 7 + test_cases/q4/graph_backtrack.test | 32 + test_cases/q4/graph_manypaths.solution | 7 + test_cases/q4/graph_manypaths.test | 39 + test_cases/q5/CONFIG | 3 + test_cases/q5/corner_tiny_corner.solution | 2 + test_cases/q5/corner_tiny_corner.test | 14 + test_cases/q6/CONFIG | 3 + test_cases/q6/corner_sanity_1.solution | 7 + test_cases/q6/corner_sanity_1.test | 12 + test_cases/q6/corner_sanity_2.solution | 7 + test_cases/q6/corner_sanity_2.test | 12 + test_cases/q6/corner_sanity_3.solution | 9 + test_cases/q6/corner_sanity_3.test | 15 + test_cases/q6/medium_corners.solution | 16 + test_cases/q6/medium_corners.test | 19 + test_cases/q7/CONFIG | 3 + test_cases/q7/food_heuristic_1.solution | 2 + test_cases/q7/food_heuristic_1.test | 13 + test_cases/q7/food_heuristic_10.solution | 2 + test_cases/q7/food_heuristic_10.test | 13 + test_cases/q7/food_heuristic_11.solution | 2 + test_cases/q7/food_heuristic_11.test | 13 + test_cases/q7/food_heuristic_12.solution | 2 + test_cases/q7/food_heuristic_12.test | 13 + test_cases/q7/food_heuristic_13.solution | 2 + test_cases/q7/food_heuristic_13.test | 13 + test_cases/q7/food_heuristic_14.solution | 2 + test_cases/q7/food_heuristic_14.test | 19 + test_cases/q7/food_heuristic_15.solution | 2 + test_cases/q7/food_heuristic_15.test | 32 + test_cases/q7/food_heuristic_16.solution | 2 + test_cases/q7/food_heuristic_16.test | 15 + test_cases/q7/food_heuristic_17.solution | 2 + test_cases/q7/food_heuristic_17.test | 14 + test_cases/q7/food_heuristic_2.solution | 2 + test_cases/q7/food_heuristic_2.test | 32 + test_cases/q7/food_heuristic_3.solution | 2 + test_cases/q7/food_heuristic_3.test | 15 + test_cases/q7/food_heuristic_4.solution | 2 + test_cases/q7/food_heuristic_4.test | 14 + test_cases/q7/food_heuristic_5.solution | 2 + test_cases/q7/food_heuristic_5.test | 13 + test_cases/q7/food_heuristic_6.solution | 2 + test_cases/q7/food_heuristic_6.test | 13 + test_cases/q7/food_heuristic_7.solution | 2 + test_cases/q7/food_heuristic_7.test | 13 + test_cases/q7/food_heuristic_8.solution | 2 + test_cases/q7/food_heuristic_8.test | 13 + test_cases/q7/food_heuristic_9.solution | 2 + test_cases/q7/food_heuristic_9.test | 13 + .../q7/food_heuristic_grade_tricky.solution | 2 + .../q7/food_heuristic_grade_tricky.test | 19 + test_cases/q8/CONFIG | 2 + test_cases/q8/closest_dot_1.solution | 2 + test_cases/q8/closest_dot_1.test | 11 + test_cases/q8/closest_dot_10.solution | 2 + test_cases/q8/closest_dot_10.test | 17 + test_cases/q8/closest_dot_11.solution | 2 + test_cases/q8/closest_dot_11.test | 30 + test_cases/q8/closest_dot_12.solution | 2 + test_cases/q8/closest_dot_12.test | 13 + test_cases/q8/closest_dot_13.solution | 2 + test_cases/q8/closest_dot_13.test | 12 + test_cases/q8/closest_dot_2.solution | 2 + test_cases/q8/closest_dot_2.test | 11 + test_cases/q8/closest_dot_3.solution | 2 + test_cases/q8/closest_dot_3.test | 11 + test_cases/q8/closest_dot_4.solution | 2 + test_cases/q8/closest_dot_4.test | 11 + test_cases/q8/closest_dot_5.solution | 2 + test_cases/q8/closest_dot_5.test | 11 + test_cases/q8/closest_dot_6.solution | 2 + test_cases/q8/closest_dot_6.test | 11 + test_cases/q8/closest_dot_7.solution | 2 + test_cases/q8/closest_dot_7.test | 11 + test_cases/q8/closest_dot_8.solution | 2 + test_cases/q8/closest_dot_8.test | 11 + test_cases/q8/closest_dot_9.solution | 2 + test_cases/q8/closest_dot_9.test | 11 + textDisplay.py | 81 ++ util.py | 674 ++++++++++++++ 195 files changed, 8812 insertions(+) create mode 100644 .DS_Store create mode 100644 .gitignore create mode 100644 AI_A1_Definition.docx create mode 100644 VERSION create mode 100644 autograder.py create mode 100644 commands.txt create mode 100644 eightpuzzle.py create mode 100644 game.py create mode 100644 ghostAgents.py create mode 100644 grading.py create mode 100644 graphicsDisplay.py create mode 100644 graphicsUtils.py create mode 100644 keyboardAgents.py create mode 100644 layout.py create mode 100644 layouts/bigCorners.lay create mode 100644 layouts/bigMaze.lay create mode 100644 layouts/bigSafeSearch.lay create mode 100644 layouts/bigSearch.lay create mode 100644 layouts/boxSearch.lay create mode 100644 layouts/capsuleClassic.lay create mode 100644 layouts/contestClassic.lay create mode 100644 layouts/contoursMaze.lay create mode 100644 layouts/greedySearch.lay create mode 100644 layouts/mediumClassic.lay create mode 100644 layouts/mediumCorners.lay create mode 100644 layouts/mediumDottedMaze.lay create mode 100644 layouts/mediumMaze.lay create mode 100644 layouts/mediumSafeSearch.lay create mode 100644 layouts/mediumScaryMaze.lay create mode 100644 layouts/mediumSearch.lay create mode 100644 layouts/minimaxClassic.lay create mode 100644 layouts/oddSearch.lay create mode 100644 layouts/openClassic.lay create mode 100644 layouts/openMaze.lay create mode 100644 layouts/openSearch.lay create mode 100644 layouts/originalClassic.lay create mode 100644 layouts/powerClassic.lay create mode 100644 layouts/smallClassic.lay create mode 100644 layouts/smallMaze.lay create mode 100644 layouts/smallSafeSearch.lay create mode 100644 layouts/smallSearch.lay create mode 100644 layouts/testClassic.lay create mode 100644 layouts/testMaze.lay create mode 100644 layouts/testSearch.lay create mode 100644 layouts/tinyCorners.lay create mode 100644 layouts/tinyMaze.lay create mode 100644 layouts/tinySafeSearch.lay create mode 100644 layouts/tinySearch.lay create mode 100644 layouts/trappedClassic.lay create mode 100644 layouts/trickyClassic.lay create mode 100644 layouts/trickySearch.lay create mode 100644 pacman.py create mode 100644 pacmanAgents.py create mode 100644 projectParams.py create mode 100644 search.py create mode 100644 searchAgents.py create mode 100644 searchTestClasses.py create mode 100644 submission_autograder.py create mode 100644 testClasses.py create mode 100644 testParser.py create mode 100644 test_cases/CONFIG create mode 100644 test_cases/q1/CONFIG create mode 100644 test_cases/q1/graph_backtrack.solution create mode 100644 test_cases/q1/graph_backtrack.test create mode 100644 test_cases/q1/graph_bfs_vs_dfs.solution create mode 100644 test_cases/q1/graph_bfs_vs_dfs.test create mode 100644 test_cases/q1/graph_infinite.solution create mode 100644 test_cases/q1/graph_infinite.test create mode 100644 test_cases/q1/graph_manypaths.solution create mode 100644 test_cases/q1/graph_manypaths.test create mode 100644 test_cases/q1/pacman_1.solution create mode 100644 test_cases/q1/pacman_1.test create mode 100644 test_cases/q2/CONFIG create mode 100644 test_cases/q2/graph_backtrack.solution create mode 100644 test_cases/q2/graph_backtrack.test create mode 100644 test_cases/q2/graph_bfs_vs_dfs.solution create mode 100644 test_cases/q2/graph_bfs_vs_dfs.test create mode 100644 test_cases/q2/graph_infinite.solution create mode 100644 test_cases/q2/graph_infinite.test create mode 100644 test_cases/q2/graph_manypaths.solution create mode 100644 test_cases/q2/graph_manypaths.test create mode 100644 test_cases/q2/pacman_1.solution create mode 100644 test_cases/q2/pacman_1.test create mode 100644 test_cases/q3/CONFIG create mode 100644 test_cases/q3/graph_backtrack.solution create mode 100644 test_cases/q3/graph_backtrack.test create mode 100644 test_cases/q3/graph_bfs_vs_dfs.solution create mode 100644 test_cases/q3/graph_bfs_vs_dfs.test create mode 100644 test_cases/q3/graph_infinite.solution create mode 100644 test_cases/q3/graph_infinite.test create mode 100644 test_cases/q3/graph_manypaths.solution create mode 100644 test_cases/q3/graph_manypaths.test create mode 100644 test_cases/q3/ucs_0_graph.solution create mode 100644 test_cases/q3/ucs_0_graph.test create mode 100644 test_cases/q3/ucs_1_problemC.solution create mode 100644 test_cases/q3/ucs_1_problemC.test create mode 100644 test_cases/q3/ucs_2_problemE.solution create mode 100644 test_cases/q3/ucs_2_problemE.test create mode 100644 test_cases/q3/ucs_3_problemW.solution create mode 100644 test_cases/q3/ucs_3_problemW.test create mode 100644 test_cases/q3/ucs_4_testSearch.solution create mode 100644 test_cases/q3/ucs_4_testSearch.test create mode 100644 test_cases/q3/ucs_5_goalAtDequeue.solution create mode 100644 test_cases/q3/ucs_5_goalAtDequeue.test create mode 100644 test_cases/q4/CONFIG create mode 100644 test_cases/q4/astar_0.solution create mode 100644 test_cases/q4/astar_0.test create mode 100644 test_cases/q4/astar_1_graph_heuristic.solution create mode 100644 test_cases/q4/astar_1_graph_heuristic.test create mode 100644 test_cases/q4/astar_2_manhattan.solution create mode 100644 test_cases/q4/astar_2_manhattan.test create mode 100644 test_cases/q4/astar_3_goalAtDequeue.solution create mode 100644 test_cases/q4/astar_3_goalAtDequeue.test create mode 100644 test_cases/q4/graph_backtrack.solution create mode 100644 test_cases/q4/graph_backtrack.test create mode 100644 test_cases/q4/graph_manypaths.solution create mode 100644 test_cases/q4/graph_manypaths.test create mode 100644 test_cases/q5/CONFIG create mode 100644 test_cases/q5/corner_tiny_corner.solution create mode 100644 test_cases/q5/corner_tiny_corner.test create mode 100644 test_cases/q6/CONFIG create mode 100644 test_cases/q6/corner_sanity_1.solution create mode 100644 test_cases/q6/corner_sanity_1.test create mode 100644 test_cases/q6/corner_sanity_2.solution create mode 100644 test_cases/q6/corner_sanity_2.test create mode 100644 test_cases/q6/corner_sanity_3.solution create mode 100644 test_cases/q6/corner_sanity_3.test create mode 100644 test_cases/q6/medium_corners.solution create mode 100644 test_cases/q6/medium_corners.test create mode 100644 test_cases/q7/CONFIG create mode 100644 test_cases/q7/food_heuristic_1.solution create mode 100644 test_cases/q7/food_heuristic_1.test create mode 100644 test_cases/q7/food_heuristic_10.solution create mode 100644 test_cases/q7/food_heuristic_10.test create mode 100644 test_cases/q7/food_heuristic_11.solution create mode 100644 test_cases/q7/food_heuristic_11.test create mode 100644 test_cases/q7/food_heuristic_12.solution create mode 100644 test_cases/q7/food_heuristic_12.test create mode 100644 test_cases/q7/food_heuristic_13.solution create mode 100644 test_cases/q7/food_heuristic_13.test create mode 100644 test_cases/q7/food_heuristic_14.solution create mode 100644 test_cases/q7/food_heuristic_14.test create mode 100644 test_cases/q7/food_heuristic_15.solution create mode 100644 test_cases/q7/food_heuristic_15.test create mode 100644 test_cases/q7/food_heuristic_16.solution create mode 100644 test_cases/q7/food_heuristic_16.test create mode 100644 test_cases/q7/food_heuristic_17.solution create mode 100644 test_cases/q7/food_heuristic_17.test create mode 100644 test_cases/q7/food_heuristic_2.solution create mode 100644 test_cases/q7/food_heuristic_2.test create mode 100644 test_cases/q7/food_heuristic_3.solution create mode 100644 test_cases/q7/food_heuristic_3.test create mode 100644 test_cases/q7/food_heuristic_4.solution create mode 100644 test_cases/q7/food_heuristic_4.test create mode 100644 test_cases/q7/food_heuristic_5.solution create mode 100644 test_cases/q7/food_heuristic_5.test create mode 100644 test_cases/q7/food_heuristic_6.solution create mode 100644 test_cases/q7/food_heuristic_6.test create mode 100644 test_cases/q7/food_heuristic_7.solution create mode 100644 test_cases/q7/food_heuristic_7.test create mode 100644 test_cases/q7/food_heuristic_8.solution create mode 100644 test_cases/q7/food_heuristic_8.test create mode 100644 test_cases/q7/food_heuristic_9.solution create mode 100644 test_cases/q7/food_heuristic_9.test create mode 100644 test_cases/q7/food_heuristic_grade_tricky.solution create mode 100644 test_cases/q7/food_heuristic_grade_tricky.test create mode 100644 test_cases/q8/CONFIG create mode 100644 test_cases/q8/closest_dot_1.solution create mode 100644 test_cases/q8/closest_dot_1.test create mode 100644 test_cases/q8/closest_dot_10.solution create mode 100644 test_cases/q8/closest_dot_10.test create mode 100644 test_cases/q8/closest_dot_11.solution create mode 100644 test_cases/q8/closest_dot_11.test create mode 100644 test_cases/q8/closest_dot_12.solution create mode 100644 test_cases/q8/closest_dot_12.test create mode 100644 test_cases/q8/closest_dot_13.solution create mode 100644 test_cases/q8/closest_dot_13.test create mode 100644 test_cases/q8/closest_dot_2.solution create mode 100644 test_cases/q8/closest_dot_2.test create mode 100644 test_cases/q8/closest_dot_3.solution create mode 100644 test_cases/q8/closest_dot_3.test create mode 100644 test_cases/q8/closest_dot_4.solution create mode 100644 test_cases/q8/closest_dot_4.test create mode 100644 test_cases/q8/closest_dot_5.solution create mode 100644 test_cases/q8/closest_dot_5.test create mode 100644 test_cases/q8/closest_dot_6.solution create mode 100644 test_cases/q8/closest_dot_6.test create mode 100644 test_cases/q8/closest_dot_7.solution create mode 100644 test_cases/q8/closest_dot_7.test create mode 100644 test_cases/q8/closest_dot_8.solution create mode 100644 test_cases/q8/closest_dot_8.test create mode 100644 test_cases/q8/closest_dot_9.solution create mode 100644 test_cases/q8/closest_dot_9.test create mode 100644 textDisplay.py create mode 100644 util.py diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..d274221c49a1705a23860740d81453d73d14cc05 GIT binary patch literal 8196 zcmeHM!EO^V5FICr(gd0Y1R`+SBd1C|BcxE$Loa|Ra1EQJgg}~&vWe7i+Dp}6;1>`d z!^a@b@Mdg9Uaza=gb=l(^{m$OX2#EBZ)ZzH>To?iAnFp)4lb6Ro9H}^$N4I?ntAdN zQUOmir3uX`r6rw~w8MkIKwuy+5Euvy1P1;E2Jp`2wsXaMUyp`9Fc28HmJIOu5aVLm zGcwmwjt+EE0zgK1EDiUl0|XNr*)uZNQlPM@s|RJE%9I#N!!aJSI%LntTuTinrQxKE zXJslBCEkIHMV(Yn%g_e~0t00RxOQ*RKI--GF6wW4!c6>&F4PV8-X;4c*)puK?a&-l&o%;QttcYHR0OW@J3ec@=g z#Y>9FR$yuZ)EsvlXK8lohl6cFU-QAdPJEbJ;$ZF=%mQj(&e6RBqGBZ_-)wXT=q1?r2wq0u@}=;?S>$DuIM409)`Z@vU`AM1 zPrS0HY{XsxQ4z1?xQ)JBK);4>&fjOkaVBHlxD{u=ZKn&}HS*w0axg>B=fVIp`J??V zJHoctLKHfd0eDPwB*nFs9bseITTEsAHQos}c6W=_E5@tfJ(*+=*huXaCsFnOlkB&# z4Pl4(n`fmZ^NZSV>4;v@2~Oh|^t{N1WWKG935FIJFbveS>~j5oboKZDz*k@(Fz_EU zprZas{}2;2wRLH#Tx-X;-r?fLdbySYK_}%nq?F^3+kY71K87k|dPe42;tASc{}8}$ Raqr(R;r%ZQ61r=_z;DOB;$Q#( literal 0 HcmV?d00001 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2971fa --- /dev/null +++ b/.gitignore @@ -0,0 +1,149 @@ +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# CMake +cmake-build-debug/ + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ \ No newline at end of file diff --git a/AI_A1_Definition.docx b/AI_A1_Definition.docx new file mode 100644 index 0000000000000000000000000000000000000000..b8b0259b671632b793943469d5ab73c1ed7493d1 GIT binary patch literal 210061 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

3^v7;@Z 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

w9 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 FzW`<6?)Lxy literal 0 HcmV?d00001 diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..52f6cb3 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +v1.001 diff --git a/autograder.py b/autograder.py new file mode 100644 index 0000000..4abe64d --- /dev/null +++ b/autograder.py @@ -0,0 +1,358 @@ +# autograder.py +# ------------- +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). + + +# imports from python standard library +import grading +import imp +import optparse +import os +import re +import sys +import projectParams +import random +random.seed(0) +try: + from pacman import GameState +except: + pass + +# register arguments and set default values +def readCommand(argv): + parser = optparse.OptionParser(description = 'Run public tests on student code') + parser.set_defaults(generateSolutions=False, edxOutput=False, gsOutput=False, muteOutput=False, printTestCase=False, noGraphics=False) + parser.add_option('--test-directory', + dest = 'testRoot', + default = 'test_cases', + help = 'Root test directory which contains subdirectories corresponding to each question') + parser.add_option('--student-code', + dest = 'studentCode', + default = projectParams.STUDENT_CODE_DEFAULT, + help = 'comma separated list of student code files') + parser.add_option('--code-directory', + dest = 'codeRoot', + default = "", + help = 'Root directory containing the student and testClass code') + parser.add_option('--test-case-code', + dest = 'testCaseCode', + default = projectParams.PROJECT_TEST_CLASSES, + help = 'class containing testClass classes for this project') + parser.add_option('--generate-solutions', + dest = 'generateSolutions', + action = 'store_true', + help = 'Write solutions generated to .solution file') + parser.add_option('--edx-output', + dest = 'edxOutput', + action = 'store_true', + help = 'Generate edX output files') + parser.add_option('--gradescope-output', + dest = 'gsOutput', + action = 'store_true', + help = 'Generate GradeScope output files') + parser.add_option('--mute', + dest = 'muteOutput', + action = 'store_true', + help = 'Mute output from executing tests') + parser.add_option('--print-tests', '-p', + dest = 'printTestCase', + action = 'store_true', + help = 'Print each test case before running them.') + parser.add_option('--test', '-t', + dest = 'runTest', + default = None, + help = 'Run one particular test. Relative to test root.') + parser.add_option('--question', '-q', + dest = 'gradeQuestion', + default = None, + help = 'Grade one particular question.') + parser.add_option('--no-graphics', + dest = 'noGraphics', + action = 'store_true', + help = 'No graphics display for pacman games.') + (options, args) = parser.parse_args(argv) + return options + + +# confirm we should author solution files +def confirmGenerate(): + print 'WARNING: this action will overwrite any solution files.' + print 'Are you sure you want to proceed? (yes/no)' + while True: + ans = sys.stdin.readline().strip() + if ans == 'yes': + break + elif ans == 'no': + sys.exit(0) + else: + print 'please answer either "yes" or "no"' + + +# TODO: Fix this so that it tracebacks work correctly +# Looking at source of the traceback module, presuming it works +# the same as the intepreters, it uses co_filename. This is, +# however, a readonly attribute. +def setModuleName(module, filename): + functionType = type(confirmGenerate) + classType = type(optparse.Option) + + for i in dir(module): + o = getattr(module, i) + if hasattr(o, '__file__'): continue + + if type(o) == functionType: + setattr(o, '__file__', filename) + elif type(o) == classType: + setattr(o, '__file__', filename) + # TODO: assign member __file__'s? + #print i, type(o) + + +#from cStringIO import StringIO + +def loadModuleString(moduleSource): + # Below broken, imp doesn't believe its being passed a file: + # ValueError: load_module arg#2 should be a file or None + # + #f = StringIO(moduleCodeDict[k]) + #tmp = imp.load_module(k, f, k, (".py", "r", imp.PY_SOURCE)) + tmp = imp.new_module(k) + exec moduleCodeDict[k] in tmp.__dict__ + setModuleName(tmp, k) + return tmp + +import py_compile + +def loadModuleFile(moduleName, filePath): + with open(filePath, 'r') as f: + return imp.load_module(moduleName, f, "%s.py" % moduleName, (".py", "r", imp.PY_SOURCE)) + + +def readFile(path, root=""): + "Read file from disk at specified path and return as string" + with open(os.path.join(root, path), 'r') as handle: + return handle.read() + + +####################################################################### +# Error Hint Map +####################################################################### + +# TODO: use these +ERROR_HINT_MAP = { + 'q1': { + "": """ + We noticed that your project threw an IndexError on q1. + While many things may cause this, it may have been from + assuming a certain number of successors from a state space + or assuming a certain number of actions available from a given + state. Try making your code more general (no hardcoded indices) + and submit again! + """ + }, + 'q3': { + "": """ + We noticed that your project threw an AttributeError on q3. + While many things may cause this, it may have been from assuming + a certain size or structure to the state space. For example, if you have + a line of code assuming that the state is (x, y) and we run your code + on a state space with (x, y, z), this error could be thrown. Try + making your code more general and submit again! + + """ + } +} + +import pprint + +def splitStrings(d): + d2 = dict(d) + for k in d: + if k[0:2] == "__": + del d2[k] + continue + if d2[k].find("\n") >= 0: + d2[k] = d2[k].split("\n") + return d2 + + +def printTest(testDict, solutionDict): + pp = pprint.PrettyPrinter(indent=4) + print "Test case:" + for line in testDict["__raw_lines__"]: + print " |", line + print "Solution:" + for line in solutionDict["__raw_lines__"]: + print " |", line + + +def runTest(testName, moduleDict, printTestCase=False, display=None): + import testParser + import testClasses + for module in moduleDict: + setattr(sys.modules[__name__], module, moduleDict[module]) + + testDict = testParser.TestParser(testName + ".test").parse() + solutionDict = testParser.TestParser(testName + ".solution").parse() + test_out_file = os.path.join('%s.test_output' % testName) + testDict['test_out_file'] = test_out_file + testClass = getattr(projectTestClasses, testDict['class']) + + questionClass = getattr(testClasses, 'Question') + question = questionClass({'max_points': 0}, display) + testCase = testClass(question, testDict) + + if printTestCase: + printTest(testDict, solutionDict) + + # This is a fragile hack to create a stub grades object + grades = grading.Grades(projectParams.PROJECT_NAME, [(None,0)]) + testCase.execute(grades, moduleDict, solutionDict) + + +# returns all the tests you need to run in order to run question +def getDepends(testParser, testRoot, question): + allDeps = [question] + questionDict = testParser.TestParser(os.path.join(testRoot, question, 'CONFIG')).parse() + if 'depends' in questionDict: + depends = questionDict['depends'].split() + for d in depends: + # run dependencies first + allDeps = getDepends(testParser, testRoot, d) + allDeps + return allDeps + +# get list of questions to grade +def getTestSubdirs(testParser, testRoot, questionToGrade): + problemDict = testParser.TestParser(os.path.join(testRoot, 'CONFIG')).parse() + if questionToGrade != None: + questions = getDepends(testParser, testRoot, questionToGrade) + if len(questions) > 1: + print 'Note: due to dependencies, the following tests will be run: %s' % ' '.join(questions) + return questions + if 'order' in problemDict: + return problemDict['order'].split() + return sorted(os.listdir(testRoot)) + + +# evaluate student code +def evaluate(generateSolutions, testRoot, moduleDict, exceptionMap=ERROR_HINT_MAP, + edxOutput=False, muteOutput=False, gsOutput=False, + printTestCase=False, questionToGrade=None, display=None): + # imports of testbench code. note that the testClasses import must follow + # the import of student code due to dependencies + import testParser + import testClasses + for module in moduleDict: + setattr(sys.modules[__name__], module, moduleDict[module]) + + questions = [] + questionDicts = {} + test_subdirs = getTestSubdirs(testParser, testRoot, questionToGrade) + for q in test_subdirs: + subdir_path = os.path.join(testRoot, q) + if not os.path.isdir(subdir_path) or q[0] == '.': + continue + + # create a question object + questionDict = testParser.TestParser(os.path.join(subdir_path, 'CONFIG')).parse() + questionClass = getattr(testClasses, questionDict['class']) + question = questionClass(questionDict, display) + questionDicts[q] = questionDict + + # load test cases into question + tests = filter(lambda t: re.match('[^#~.].*\.test\Z', t), os.listdir(subdir_path)) + tests = map(lambda t: re.match('(.*)\.test\Z', t).group(1), tests) + for t in sorted(tests): + test_file = os.path.join(subdir_path, '%s.test' % t) + solution_file = os.path.join(subdir_path, '%s.solution' % t) + test_out_file = os.path.join(subdir_path, '%s.test_output' % t) + testDict = testParser.TestParser(test_file).parse() + if testDict.get("disabled", "false").lower() == "true": + continue + testDict['test_out_file'] = test_out_file + testClass = getattr(projectTestClasses, testDict['class']) + testCase = testClass(question, testDict) + def makefun(testCase, solution_file): + if generateSolutions: + # write solution file to disk + return lambda grades: testCase.writeSolution(moduleDict, solution_file) + else: + # read in solution dictionary and pass as an argument + testDict = testParser.TestParser(test_file).parse() + solutionDict = testParser.TestParser(solution_file).parse() + if printTestCase: + return lambda grades: printTest(testDict, solutionDict) or testCase.execute(grades, moduleDict, solutionDict) + else: + return lambda grades: testCase.execute(grades, moduleDict, solutionDict) + question.addTestCase(testCase, makefun(testCase, solution_file)) + + # Note extra function is necessary for scoping reasons + def makefun(question): + return lambda grades: question.execute(grades) + setattr(sys.modules[__name__], q, makefun(question)) + questions.append((q, question.getMaxPoints())) + + grades = grading.Grades(projectParams.PROJECT_NAME, questions, + gsOutput=gsOutput, edxOutput=edxOutput, muteOutput=muteOutput) + if questionToGrade == None: + for q in questionDicts: + for prereq in questionDicts[q].get('depends', '').split(): + grades.addPrereq(q, prereq) + + grades.grade(sys.modules[__name__], bonusPic = projectParams.BONUS_PIC) + return grades.points + + + +def getDisplay(graphicsByDefault, options=None): + graphics = graphicsByDefault + if options is not None and options.noGraphics: + graphics = False + if graphics: + try: + import graphicsDisplay + return graphicsDisplay.PacmanGraphics(1, frameTime=.05) + except ImportError: + pass + import textDisplay + return textDisplay.NullGraphics() + + + + +if __name__ == '__main__': + options = readCommand(sys.argv) + if options.generateSolutions: + confirmGenerate() + codePaths = options.studentCode.split(',') + # moduleCodeDict = {} + # for cp in codePaths: + # moduleName = re.match('.*?([^/]*)\.py', cp).group(1) + # moduleCodeDict[moduleName] = readFile(cp, root=options.codeRoot) + # moduleCodeDict['projectTestClasses'] = readFile(options.testCaseCode, root=options.codeRoot) + # moduleDict = loadModuleDict(moduleCodeDict) + + moduleDict = {} + for cp in codePaths: + moduleName = re.match('.*?([^/]*)\.py', cp).group(1) + moduleDict[moduleName] = loadModuleFile(moduleName, os.path.join(options.codeRoot, cp)) + moduleName = re.match('.*?([^/]*)\.py', options.testCaseCode).group(1) + moduleDict['projectTestClasses'] = loadModuleFile(moduleName, os.path.join(options.codeRoot, options.testCaseCode)) + + + if options.runTest != None: + runTest(options.runTest, moduleDict, printTestCase=options.printTestCase, display=getDisplay(True, options)) + else: + evaluate(options.generateSolutions, options.testRoot, moduleDict, + gsOutput=options.gsOutput, + edxOutput=options.edxOutput, muteOutput=options.muteOutput, printTestCase=options.printTestCase, + questionToGrade=options.gradeQuestion, display=getDisplay(options.gradeQuestion!=None, options)) diff --git a/commands.txt b/commands.txt new file mode 100644 index 0000000..d5c70e2 --- /dev/null +++ b/commands.txt @@ -0,0 +1,22 @@ +python pacman.py +python pacman.py --layout testMaze --pacman GoWestAgent +python pacman.py --layout tinyMaze --pacman GoWestAgent +python pacman.py -h +python pacman.py -l tinyMaze -p SearchAgent -a fn=tinyMazeSearch +python pacman.py -l tinyMaze -p SearchAgent +python pacman.py -l mediumMaze -p SearchAgent +python pacman.py -l bigMaze -z .5 -p SearchAgent +python pacman.py -l mediumMaze -p SearchAgent -a fn=bfs +python pacman.py -l bigMaze -p SearchAgent -a fn=bfs -z .5 +python eightpuzzle.py +python pacman.py -l mediumMaze -p SearchAgent -a fn=ucs +python pacman.py -l mediumDottedMaze -p StayEastSearchAgent +python pacman.py -l mediumScaryMaze -p StayWestSearchAgent +python pacman.py -l bigMaze -z .5 -p SearchAgent -a fn=astar,heuristic=manhattanHeuristic +python pacman.py -l tinyCorners -p SearchAgent -a fn=bfs,prob=CornersProblem +python pacman.py -l mediumCorners -p SearchAgent -a fn=bfs,prob=CornersProblem +python pacman.py -l mediumCorners -p AStarCornersAgent -z 0.5 +python pacman.py -l testSearch -p AStarFoodSearchAgent +python pacman.py -l trickySearch -p AStarFoodSearchAgent +python pacman.py -l bigSearch -p ClosestDotSearchAgent -z .5 +python pacman.py -l bigSearch -p ApproximateSearchAgent -z .5 -q diff --git a/eightpuzzle.py b/eightpuzzle.py new file mode 100644 index 0000000..6aa376c --- /dev/null +++ b/eightpuzzle.py @@ -0,0 +1,281 @@ +# eightpuzzle.py +# -------------- +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). + + +import search +import random + +# Module Classes + +class EightPuzzleState: + """ + The Eight Puzzle is described in the course textbook on + page 64. + + This class defines the mechanics of the puzzle itself. The + task of recasting this puzzle as a search problem is left to + the EightPuzzleSearchProblem class. + """ + + def __init__( self, numbers ): + """ + Constructs a new eight puzzle from an ordering of numbers. + + numbers: a list of integers from 0 to 8 representing an + instance of the eight puzzle. 0 represents the blank + space. Thus, the list + + [1, 0, 2, 3, 4, 5, 6, 7, 8] + + represents the eight puzzle: + ------------- + | 1 | | 2 | + ------------- + | 3 | 4 | 5 | + ------------- + | 6 | 7 | 8 | + ------------ + + The configuration of the puzzle is stored in a 2-dimensional + list (a list of lists) 'cells'. + """ + self.cells = [] + numbers = numbers[:] # Make a copy so as not to cause side-effects. + numbers.reverse() + for row in range( 3 ): + self.cells.append( [] ) + for col in range( 3 ): + self.cells[row].append( numbers.pop() ) + if self.cells[row][col] == 0: + self.blankLocation = row, col + + def isGoal( self ): + """ + Checks to see if the puzzle is in its goal state. + + ------------- + | | 1 | 2 | + ------------- + | 3 | 4 | 5 | + ------------- + | 6 | 7 | 8 | + ------------- + + >>> EightPuzzleState([0, 1, 2, 3, 4, 5, 6, 7, 8]).isGoal() + True + + >>> EightPuzzleState([1, 0, 2, 3, 4, 5, 6, 7, 8]).isGoal() + False + """ + current = 0 + for row in range( 3 ): + for col in range( 3 ): + if current != self.cells[row][col]: + return False + current += 1 + return True + + def legalMoves( self ): + """ + Returns a list of legal moves from the current state. + + Moves consist of moving the blank space up, down, left or right. + These are encoded as 'up', 'down', 'left' and 'right' respectively. + + >>> EightPuzzleState([0, 1, 2, 3, 4, 5, 6, 7, 8]).legalMoves() + ['down', 'right'] + """ + moves = [] + row, col = self.blankLocation + if(row != 0): + moves.append('up') + if(row != 2): + moves.append('down') + if(col != 0): + moves.append('left') + if(col != 2): + moves.append('right') + return moves + + def result(self, move): + """ + Returns a new eightPuzzle with the current state and blankLocation + updated based on the provided move. + + The move should be a string drawn from a list returned by legalMoves. + Illegal moves will raise an exception, which may be an array bounds + exception. + + NOTE: This function *does not* change the current object. Instead, + it returns a new object. + """ + row, col = self.blankLocation + if(move == 'up'): + newrow = row - 1 + newcol = col + elif(move == 'down'): + newrow = row + 1 + newcol = col + elif(move == 'left'): + newrow = row + newcol = col - 1 + elif(move == 'right'): + newrow = row + newcol = col + 1 + else: + raise "Illegal Move" + + # Create a copy of the current eightPuzzle + newPuzzle = EightPuzzleState([0, 0, 0, 0, 0, 0, 0, 0, 0]) + newPuzzle.cells = [values[:] for values in self.cells] + # And update it to reflect the move + newPuzzle.cells[row][col] = self.cells[newrow][newcol] + newPuzzle.cells[newrow][newcol] = self.cells[row][col] + newPuzzle.blankLocation = newrow, newcol + + return newPuzzle + + # Utilities for comparison and display + def __eq__(self, other): + """ + Overloads '==' such that two eightPuzzles with the same configuration + are equal. + + >>> EightPuzzleState([0, 1, 2, 3, 4, 5, 6, 7, 8]) == \ + EightPuzzleState([1, 0, 2, 3, 4, 5, 6, 7, 8]).result('left') + True + """ + for row in range( 3 ): + if self.cells[row] != other.cells[row]: + return False + return True + + def __hash__(self): + return hash(str(self.cells)) + + def __getAsciiString(self): + """ + Returns a display string for the maze + """ + lines = [] + horizontalLine = ('-' * (13)) + lines.append(horizontalLine) + for row in self.cells: + rowLine = '|' + for col in row: + if col == 0: + col = ' ' + rowLine = rowLine + ' ' + col.__str__() + ' |' + lines.append(rowLine) + lines.append(horizontalLine) + return '\n'.join(lines) + + def __str__(self): + return self.__getAsciiString() + +# TODO: Implement The methods in this class + +class EightPuzzleSearchProblem(search.SearchProblem): + """ + Implementation of a SearchProblem for the Eight Puzzle domain + + Each state is represented by an instance of an eightPuzzle. + """ + def __init__(self,puzzle): + "Creates a new EightPuzzleSearchProblem which stores search information." + self.puzzle = puzzle + + def getStartState(self): + return puzzle + + def isGoalState(self,state): + return state.isGoal() + + def getSuccessors(self,state): + """ + Returns list of (successor, action, stepCost) pairs where + each succesor is either left, right, up, or down + from the original state and the cost is 1.0 for each + """ + succ = [] + for a in state.legalMoves(): + succ.append((state.result(a), a, 1)) + return succ + + def getCostOfActions(self, actions): + """ + actions: A list of actions to take + + This method returns the total cost of a particular sequence of actions. The sequence must + be composed of legal moves + """ + return len(actions) + +EIGHT_PUZZLE_DATA = [[1, 0, 2, 3, 4, 5, 6, 7, 8], + [1, 7, 8, 2, 3, 4, 5, 6, 0], + [4, 3, 2, 7, 0, 5, 1, 6, 8], + [5, 1, 3, 4, 0, 2, 6, 7, 8], + [1, 2, 5, 7, 6, 8, 0, 4, 3], + [0, 3, 1, 6, 8, 2, 7, 5, 4]] + +def loadEightPuzzle(puzzleNumber): + """ + puzzleNumber: The number of the eight puzzle to load. + + Returns an eight puzzle object generated from one of the + provided puzzles in EIGHT_PUZZLE_DATA. + + puzzleNumber can range from 0 to 5. + + >>> print loadEightPuzzle(0) + ------------- + | 1 | | 2 | + ------------- + | 3 | 4 | 5 | + ------------- + | 6 | 7 | 8 | + ------------- + """ + return EightPuzzleState(EIGHT_PUZZLE_DATA[puzzleNumber]) + +def createRandomEightPuzzle(moves=100): + """ + moves: number of random moves to apply + + Creates a random eight puzzle by applying + a series of 'moves' random moves to a solved + puzzle. + """ + puzzle = EightPuzzleState([0,1,2,3,4,5,6,7,8]) + for i in range(moves): + # Execute a random legal move + puzzle = puzzle.result(random.sample(puzzle.legalMoves(), 1)[0]) + return puzzle + +if __name__ == '__main__': + puzzle = createRandomEightPuzzle(25) + print('A random puzzle:') + print(puzzle) + + problem = EightPuzzleSearchProblem(puzzle) + path = search.breadthFirstSearch(problem) + print('BFS found a path of %d moves: %s' % (len(path), str(path))) + curr = puzzle + i = 1 + for a in path: + curr = curr.result(a) + print('After %d move%s: %s' % (i, ("", "s")[i>1], a)) + print(curr) + + raw_input("Press return for the next state...") # wait for key stroke + i += 1 diff --git a/game.py b/game.py new file mode 100644 index 0000000..e34d6cf --- /dev/null +++ b/game.py @@ -0,0 +1,729 @@ +# game.py +# ------- +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). + + +# game.py +# ------- +# Licensing Information: Please do not distribute or publish solutions to this +# project. You are free to use and extend these projects for educational +# purposes. The Pacman AI projects were developed at UC Berkeley, primarily by +# John DeNero (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# For more info, see http://inst.eecs.berkeley.edu/~cs188/sp09/pacman.html + +from util import * +import time, os +import traceback +import sys + +####################### +# Parts worth reading # +####################### + +class Agent: + """ + An agent must define a getAction method, but may also define the + following methods which will be called if they exist: + + def registerInitialState(self, state): # inspects the starting state + """ + def __init__(self, index=0): + self.index = index + + def getAction(self, state): + """ + The Agent will receive a GameState (from either {pacman, capture, sonar}.py) and + must return an action from Directions.{North, South, East, West, Stop} + """ + raiseNotDefined() + +class Directions: + NORTH = 'North' + SOUTH = 'South' + EAST = 'East' + WEST = 'West' + STOP = 'Stop' + + LEFT = {NORTH: WEST, + SOUTH: EAST, + EAST: NORTH, + WEST: SOUTH, + STOP: STOP} + + RIGHT = dict([(y,x) for x, y in LEFT.items()]) + + REVERSE = {NORTH: SOUTH, + SOUTH: NORTH, + EAST: WEST, + WEST: EAST, + STOP: STOP} + +class Configuration: + """ + A Configuration holds the (x,y) coordinate of a character, along with its + traveling direction. + + The convention for positions, like a graph, is that (0,0) is the lower left corner, x increases + horizontally and y increases vertically. Therefore, north is the direction of increasing y, or (0,1). + """ + + def __init__(self, pos, direction): + self.pos = pos + self.direction = direction + + def getPosition(self): + return (self.pos) + + def getDirection(self): + return self.direction + + def isInteger(self): + x,y = self.pos + return x == int(x) and y == int(y) + + def __eq__(self, other): + if other == None: return False + return (self.pos == other.pos and self.direction == other.direction) + + def __hash__(self): + x = hash(self.pos) + y = hash(self.direction) + return hash(x + 13 * y) + + def __str__(self): + return "(x,y)="+str(self.pos)+", "+str(self.direction) + + def generateSuccessor(self, vector): + """ + Generates a new configuration reached by translating the current + configuration by the action vector. This is a low-level call and does + not attempt to respect the legality of the movement. + + Actions are movement vectors. + """ + x, y= self.pos + dx, dy = vector + direction = Actions.vectorToDirection(vector) + if direction == Directions.STOP: + direction = self.direction # There is no stop direction + return Configuration((x + dx, y+dy), direction) + +class AgentState: + """ + AgentStates hold the state of an agent (configuration, speed, scared, etc). + """ + + def __init__( self, startConfiguration, isPacman ): + self.start = startConfiguration + self.configuration = startConfiguration + self.isPacman = isPacman + self.scaredTimer = 0 + self.numCarrying = 0 + self.numReturned = 0 + + def __str__( self ): + if self.isPacman: + return "Pacman: " + str( self.configuration ) + else: + return "Ghost: " + str( self.configuration ) + + def __eq__( self, other ): + if other == None: + return False + return self.configuration == other.configuration and self.scaredTimer == other.scaredTimer + + def __hash__(self): + return hash(hash(self.configuration) + 13 * hash(self.scaredTimer)) + + def copy( self ): + state = AgentState( self.start, self.isPacman ) + state.configuration = self.configuration + state.scaredTimer = self.scaredTimer + state.numCarrying = self.numCarrying + state.numReturned = self.numReturned + return state + + def getPosition(self): + if self.configuration == None: return None + return self.configuration.getPosition() + + def getDirection(self): + return self.configuration.getDirection() + +class Grid: + """ + A 2-dimensional array of objects backed by a list of lists. Data is accessed + via grid[x][y] where (x,y) are positions on a Pacman map with x horizontal, + y vertical and the origin (0,0) in the bottom left corner. + + The __str__ method constructs an output that is oriented like a pacman board. + """ + def __init__(self, width, height, initialValue=False, bitRepresentation=None): + if initialValue not in [False, True]: raise Exception('Grids can only contain booleans') + self.CELLS_PER_INT = 30 + + self.width = width + self.height = height + self.data = [[initialValue for y in range(height)] for x in range(width)] + if bitRepresentation: + self._unpackBits(bitRepresentation) + + def __getitem__(self, i): + return self.data[i] + + def __setitem__(self, key, item): + self.data[key] = item + + def __str__(self): + out = [[str(self.data[x][y])[0] for x in range(self.width)] for y in range(self.height)] + out.reverse() + return '\n'.join([''.join(x) for x in out]) + + def __eq__(self, other): + if other == None: return False + return self.data == other.data + + def __hash__(self): + # return hash(str(self)) + base = 1 + h = 0 + for l in self.data: + for i in l: + if i: + h += base + base *= 2 + return hash(h) + + def copy(self): + g = Grid(self.width, self.height) + g.data = [x[:] for x in self.data] + return g + + def deepCopy(self): + return self.copy() + + def shallowCopy(self): + g = Grid(self.width, self.height) + g.data = self.data + return g + + def count(self, item =True ): + return sum([x.count(item) for x in self.data]) + + def asList(self, key = True): + list = [] + for x in range(self.width): + for y in range(self.height): + if self[x][y] == key: list.append( (x,y) ) + return list + + def packBits(self): + """ + Returns an efficient int list representation + + (width, height, bitPackedInts...) + """ + bits = [self.width, self.height] + currentInt = 0 + for i in range(self.height * self.width): + bit = self.CELLS_PER_INT - (i % self.CELLS_PER_INT) - 1 + x, y = self._cellIndexToPosition(i) + if self[x][y]: + currentInt += 2 ** bit + if (i + 1) % self.CELLS_PER_INT == 0: + bits.append(currentInt) + currentInt = 0 + bits.append(currentInt) + return tuple(bits) + + def _cellIndexToPosition(self, index): + x = index / self.height + y = index % self.height + return x, y + + def _unpackBits(self, bits): + """ + Fills in data from a bit-level representation + """ + cell = 0 + for packed in bits: + for bit in self._unpackInt(packed, self.CELLS_PER_INT): + if cell == self.width * self.height: break + x, y = self._cellIndexToPosition(cell) + self[x][y] = bit + cell += 1 + + def _unpackInt(self, packed, size): + bools = [] + if packed < 0: raise ValueError, "must be a positive integer" + for i in range(size): + n = 2 ** (self.CELLS_PER_INT - i - 1) + if packed >= n: + bools.append(True) + packed -= n + else: + bools.append(False) + return bools + +def reconstituteGrid(bitRep): + if type(bitRep) is not type((1,2)): + return bitRep + width, height = bitRep[:2] + return Grid(width, height, bitRepresentation= bitRep[2:]) + +#################################### +# Parts you shouldn't have to read # +#################################### + +class Actions: + """ + A collection of static methods for manipulating move actions. + """ + # Directions + _directions = {Directions.NORTH: (0, 1), + Directions.SOUTH: (0, -1), + Directions.EAST: (1, 0), + Directions.WEST: (-1, 0), + Directions.STOP: (0, 0)} + + _directionsAsList = _directions.items() + + TOLERANCE = .001 + + def reverseDirection(action): + if action == Directions.NORTH: + return Directions.SOUTH + if action == Directions.SOUTH: + return Directions.NORTH + if action == Directions.EAST: + return Directions.WEST + if action == Directions.WEST: + return Directions.EAST + return action + reverseDirection = staticmethod(reverseDirection) + + def vectorToDirection(vector): + dx, dy = vector + if dy > 0: + return Directions.NORTH + if dy < 0: + return Directions.SOUTH + if dx < 0: + return Directions.WEST + if dx > 0: + return Directions.EAST + return Directions.STOP + vectorToDirection = staticmethod(vectorToDirection) + + def directionToVector(direction, speed = 1.0): + dx, dy = Actions._directions[direction] + return (dx * speed, dy * speed) + directionToVector = staticmethod(directionToVector) + + def getPossibleActions(config, walls): + possible = [] + x, y = config.pos + x_int, y_int = int(x + 0.5), int(y + 0.5) + + # In between grid points, all agents must continue straight + if (abs(x - x_int) + abs(y - y_int) > Actions.TOLERANCE): + return [config.getDirection()] + + for dir, vec in Actions._directionsAsList: + dx, dy = vec + next_y = y_int + dy + next_x = x_int + dx + if not walls[next_x][next_y]: possible.append(dir) + + return possible + + getPossibleActions = staticmethod(getPossibleActions) + + def getLegalNeighbors(position, walls): + x,y = position + x_int, y_int = int(x + 0.5), int(y + 0.5) + neighbors = [] + for dir, vec in Actions._directionsAsList: + dx, dy = vec + next_x = x_int + dx + if next_x < 0 or next_x == walls.width: continue + next_y = y_int + dy + if next_y < 0 or next_y == walls.height: continue + if not walls[next_x][next_y]: neighbors.append((next_x, next_y)) + return neighbors + getLegalNeighbors = staticmethod(getLegalNeighbors) + + def getSuccessor(position, action): + dx, dy = Actions.directionToVector(action) + x, y = position + return (x + dx, y + dy) + getSuccessor = staticmethod(getSuccessor) + +class GameStateData: + """ + + """ + def __init__( self, prevState = None ): + """ + Generates a new data packet by copying information from its predecessor. + """ + if prevState != None: + self.food = prevState.food.shallowCopy() + self.capsules = prevState.capsules[:] + self.agentStates = self.copyAgentStates( prevState.agentStates ) + self.layout = prevState.layout + self._eaten = prevState._eaten + self.score = prevState.score + + self._foodEaten = None + self._foodAdded = None + self._capsuleEaten = None + self._agentMoved = None + self._lose = False + self._win = False + self.scoreChange = 0 + + def deepCopy( self ): + state = GameStateData( self ) + state.food = self.food.deepCopy() + state.layout = self.layout.deepCopy() + state._agentMoved = self._agentMoved + state._foodEaten = self._foodEaten + state._foodAdded = self._foodAdded + state._capsuleEaten = self._capsuleEaten + return state + + def copyAgentStates( self, agentStates ): + copiedStates = [] + for agentState in agentStates: + copiedStates.append( agentState.copy() ) + return copiedStates + + def __eq__( self, other ): + """ + Allows two states to be compared. + """ + if other == None: return False + # TODO Check for type of other + if not self.agentStates == other.agentStates: return False + if not self.food == other.food: return False + if not self.capsules == other.capsules: return False + if not self.score == other.score: return False + return True + + def __hash__( self ): + """ + Allows states to be keys of dictionaries. + """ + for i, state in enumerate( self.agentStates ): + try: + int(hash(state)) + except TypeError, e: + print e + #hash(state) + return int((hash(tuple(self.agentStates)) + 13*hash(self.food) + 113* hash(tuple(self.capsules)) + 7 * hash(self.score)) % 1048575 ) + + def __str__( self ): + width, height = self.layout.width, self.layout.height + map = Grid(width, height) + if type(self.food) == type((1,2)): + self.food = reconstituteGrid(self.food) + for x in range(width): + for y in range(height): + food, walls = self.food, self.layout.walls + map[x][y] = self._foodWallStr(food[x][y], walls[x][y]) + + for agentState in self.agentStates: + if agentState == None: continue + if agentState.configuration == None: continue + x,y = [int( i ) for i in nearestPoint( agentState.configuration.pos )] + agent_dir = agentState.configuration.direction + if agentState.isPacman: + map[x][y] = self._pacStr( agent_dir ) + else: + map[x][y] = self._ghostStr( agent_dir ) + + for x, y in self.capsules: + map[x][y] = 'o' + + return str(map) + ("\nScore: %d\n" % self.score) + + def _foodWallStr( self, hasFood, hasWall ): + if hasFood: + return '.' + elif hasWall: + return '%' + else: + return ' ' + + def _pacStr( self, dir ): + if dir == Directions.NORTH: + return 'v' + if dir == Directions.SOUTH: + return '^' + if dir == Directions.WEST: + return '>' + return '<' + + def _ghostStr( self, dir ): + return 'G' + if dir == Directions.NORTH: + return 'M' + if dir == Directions.SOUTH: + return 'W' + if dir == Directions.WEST: + return '3' + return 'E' + + def initialize( self, layout, numGhostAgents ): + """ + Creates an initial game state from a layout array (see layout.py). + """ + self.food = layout.food.copy() + #self.capsules = [] + self.capsules = layout.capsules[:] + self.layout = layout + self.score = 0 + self.scoreChange = 0 + + self.agentStates = [] + numGhosts = 0 + for isPacman, pos in layout.agentPositions: + if not isPacman: + if numGhosts == numGhostAgents: continue # Max ghosts reached already + else: numGhosts += 1 + self.agentStates.append( AgentState( Configuration( pos, Directions.STOP), isPacman) ) + self._eaten = [False for a in self.agentStates] + +try: + import boinc + _BOINC_ENABLED = True +except: + _BOINC_ENABLED = False + +class Game: + """ + The Game manages the control flow, soliciting actions from agents. + """ + + def __init__( self, agents, display, rules, startingIndex=0, muteAgents=False, catchExceptions=False ): + self.agentCrashed = False + self.agents = agents + self.display = display + self.rules = rules + self.startingIndex = startingIndex + self.gameOver = False + self.muteAgents = muteAgents + self.catchExceptions = catchExceptions + self.moveHistory = [] + self.totalAgentTimes = [0 for agent in agents] + self.totalAgentTimeWarnings = [0 for agent in agents] + self.agentTimeout = False + import cStringIO + self.agentOutput = [cStringIO.StringIO() for agent in agents] + + def getProgress(self): + if self.gameOver: + return 1.0 + else: + return self.rules.getProgress(self) + + def _agentCrash( self, agentIndex, quiet=False): + "Helper method for handling agent crashes" + if not quiet: traceback.print_exc() + self.gameOver = True + self.agentCrashed = True + self.rules.agentCrash(self, agentIndex) + + OLD_STDOUT = None + OLD_STDERR = None + + def mute(self, agentIndex): + if not self.muteAgents: return + global OLD_STDOUT, OLD_STDERR + import cStringIO + OLD_STDOUT = sys.stdout + OLD_STDERR = sys.stderr + sys.stdout = self.agentOutput[agentIndex] + sys.stderr = self.agentOutput[agentIndex] + + def unmute(self): + if not self.muteAgents: return + global OLD_STDOUT, OLD_STDERR + # Revert stdout/stderr to originals + sys.stdout = OLD_STDOUT + sys.stderr = OLD_STDERR + + + def run( self ): + """ + Main control loop for game play. + """ + self.display.initialize(self.state.data) + self.numMoves = 0 + + ###self.display.initialize(self.state.makeObservation(1).data) + # inform learning agents of the game start + for i in range(len(self.agents)): + agent = self.agents[i] + if not agent: + self.mute(i) + # this is a null agent, meaning it failed to load + # the other team wins + print >>sys.stderr, "Agent %d failed to load" % i + self.unmute() + self._agentCrash(i, quiet=True) + return + if ("registerInitialState" in dir(agent)): + self.mute(i) + if self.catchExceptions: + try: + timed_func = TimeoutFunction(agent.registerInitialState, int(self.rules.getMaxStartupTime(i))) + try: + start_time = time.time() + timed_func(self.state.deepCopy()) + time_taken = time.time() - start_time + self.totalAgentTimes[i] += time_taken + except TimeoutFunctionException: + print >>sys.stderr, "Agent %d ran out of time on startup!" % i + self.unmute() + self.agentTimeout = True + self._agentCrash(i, quiet=True) + return + except Exception,data: + self._agentCrash(i, quiet=False) + self.unmute() + return + else: + agent.registerInitialState(self.state.deepCopy()) + ## TODO: could this exceed the total time + self.unmute() + + agentIndex = self.startingIndex + numAgents = len( self.agents ) + + while not self.gameOver: + # Fetch the next agent + agent = self.agents[agentIndex] + move_time = 0 + skip_action = False + # Generate an observation of the state + if 'observationFunction' in dir( agent ): + self.mute(agentIndex) + if self.catchExceptions: + try: + timed_func = TimeoutFunction(agent.observationFunction, int(self.rules.getMoveTimeout(agentIndex))) + try: + start_time = time.time() + observation = timed_func(self.state.deepCopy()) + except TimeoutFunctionException: + skip_action = True + move_time += time.time() - start_time + self.unmute() + except Exception,data: + self._agentCrash(agentIndex, quiet=False) + self.unmute() + return + else: + observation = agent.observationFunction(self.state.deepCopy()) + self.unmute() + else: + observation = self.state.deepCopy() + + # Solicit an action + action = None + self.mute(agentIndex) + if self.catchExceptions: + try: + timed_func = TimeoutFunction(agent.getAction, int(self.rules.getMoveTimeout(agentIndex)) - int(move_time)) + try: + start_time = time.time() + if skip_action: + raise TimeoutFunctionException() + action = timed_func( observation ) + except TimeoutFunctionException: + print >>sys.stderr, "Agent %d timed out on a single move!" % agentIndex + self.agentTimeout = True + self._agentCrash(agentIndex, quiet=True) + self.unmute() + return + + move_time += time.time() - start_time + + if move_time > self.rules.getMoveWarningTime(agentIndex): + self.totalAgentTimeWarnings[agentIndex] += 1 + print >>sys.stderr, "Agent %d took too long to make a move! This is warning %d" % (agentIndex, self.totalAgentTimeWarnings[agentIndex]) + if self.totalAgentTimeWarnings[agentIndex] > self.rules.getMaxTimeWarnings(agentIndex): + print >>sys.stderr, "Agent %d exceeded the maximum number of warnings: %d" % (agentIndex, self.totalAgentTimeWarnings[agentIndex]) + self.agentTimeout = True + self._agentCrash(agentIndex, quiet=True) + self.unmute() + return + + self.totalAgentTimes[agentIndex] += move_time + #print "Agent: %d, time: %f, total: %f" % (agentIndex, move_time, self.totalAgentTimes[agentIndex]) + if self.totalAgentTimes[agentIndex] > self.rules.getMaxTotalTime(agentIndex): + print >>sys.stderr, "Agent %d ran out of time! (time: %1.2f)" % (agentIndex, self.totalAgentTimes[agentIndex]) + self.agentTimeout = True + self._agentCrash(agentIndex, quiet=True) + self.unmute() + return + self.unmute() + except Exception,data: + self._agentCrash(agentIndex) + self.unmute() + return + else: + action = agent.getAction(observation) + self.unmute() + + # Execute the action + self.moveHistory.append( (agentIndex, action) ) + if self.catchExceptions: + try: + self.state = self.state.generateSuccessor( agentIndex, action ) + except Exception,data: + self.mute(agentIndex) + self._agentCrash(agentIndex) + self.unmute() + return + else: + self.state = self.state.generateSuccessor( agentIndex, action ) + + # Change the display + self.display.update( self.state.data ) + ###idx = agentIndex - agentIndex % 2 + 1 + ###self.display.update( self.state.makeObservation(idx).data ) + + # Allow for game specific conditions (winning, losing, etc.) + self.rules.process(self.state, self) + # Track progress + if agentIndex == numAgents + 1: self.numMoves += 1 + # Next agent + agentIndex = ( agentIndex + 1 ) % numAgents + + if _BOINC_ENABLED: + boinc.set_fraction_done(self.getProgress()) + + # inform a learning agent of the game result + for agentIndex, agent in enumerate(self.agents): + if "final" in dir( agent ) : + try: + self.mute(agentIndex) + agent.final( self.state ) + self.unmute() + except Exception,data: + if not self.catchExceptions: raise + self._agentCrash(agentIndex) + self.unmute() + return + self.display.finish() diff --git a/ghostAgents.py b/ghostAgents.py new file mode 100644 index 0000000..c3afe1f --- /dev/null +++ b/ghostAgents.py @@ -0,0 +1,81 @@ +# ghostAgents.py +# -------------- +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). + + +from game import Agent +from game import Actions +from game import Directions +import random +from util import manhattanDistance +import util + +class GhostAgent( Agent ): + def __init__( self, index ): + self.index = index + + def getAction( self, state ): + dist = self.getDistribution(state) + if len(dist) == 0: + return Directions.STOP + else: + return util.chooseFromDistribution( dist ) + + def getDistribution(self, state): + "Returns a Counter encoding a distribution over actions from the provided state." + util.raiseNotDefined() + +class RandomGhost( GhostAgent ): + "A ghost that chooses a legal action uniformly at random." + def getDistribution( self, state ): + dist = util.Counter() + for a in state.getLegalActions( self.index ): dist[a] = 1.0 + dist.normalize() + return dist + +class DirectionalGhost( GhostAgent ): + "A ghost that prefers to rush Pacman, or flee when scared." + def __init__( self, index, prob_attack=0.8, prob_scaredFlee=0.8 ): + self.index = index + self.prob_attack = prob_attack + self.prob_scaredFlee = prob_scaredFlee + + def getDistribution( self, state ): + # Read variables from state + ghostState = state.getGhostState( self.index ) + legalActions = state.getLegalActions( self.index ) + pos = state.getGhostPosition( self.index ) + isScared = ghostState.scaredTimer > 0 + + speed = 1 + if isScared: speed = 0.5 + + actionVectors = [Actions.directionToVector( a, speed ) for a in legalActions] + newPositions = [( pos[0]+a[0], pos[1]+a[1] ) for a in actionVectors] + pacmanPosition = state.getPacmanPosition() + + # Select best actions given the state + distancesToPacman = [manhattanDistance( pos, pacmanPosition ) for pos in newPositions] + if isScared: + bestScore = max( distancesToPacman ) + bestProb = self.prob_scaredFlee + else: + bestScore = min( distancesToPacman ) + bestProb = self.prob_attack + bestActions = [action for action, distance in zip( legalActions, distancesToPacman ) if distance == bestScore] + + # Construct distribution + dist = util.Counter() + for a in bestActions: dist[a] = bestProb / len(bestActions) + for a in legalActions: dist[a] += ( 1-bestProb ) / len(legalActions) + dist.normalize() + return dist diff --git a/grading.py b/grading.py new file mode 100644 index 0000000..b63c877 --- /dev/null +++ b/grading.py @@ -0,0 +1,323 @@ +# grading.py +# ---------- +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). + + +"Common code for autograders" + +import cgi +import time +import sys +import json +import traceback +import pdb +from collections import defaultdict +import util + +class Grades: + "A data structure for project grades, along with formatting code to display them" + def __init__(self, projectName, questionsAndMaxesList, + gsOutput=False, edxOutput=False, muteOutput=False): + """ + Defines the grading scheme for a project + projectName: project name + questionsAndMaxesDict: a list of (question name, max points per question) + """ + self.questions = [el[0] for el in questionsAndMaxesList] + self.maxes = dict(questionsAndMaxesList) + self.points = Counter() + self.messages = dict([(q, []) for q in self.questions]) + self.project = projectName + self.start = time.localtime()[1:6] + self.sane = True # Sanity checks + self.currentQuestion = None # Which question we're grading + self.edxOutput = edxOutput + self.gsOutput = gsOutput # GradeScope output + self.mute = muteOutput + self.prereqs = defaultdict(set) + + #print 'Autograder transcript for %s' % self.project + print 'Starting on %d-%d at %d:%02d:%02d' % self.start + + def addPrereq(self, question, prereq): + self.prereqs[question].add(prereq) + + def grade(self, gradingModule, exceptionMap = {}, bonusPic = False): + """ + Grades each question + gradingModule: the module with all the grading functions (pass in with sys.modules[__name__]) + """ + + completedQuestions = set([]) + for q in self.questions: + print '\nQuestion %s' % q + print '=' * (9 + len(q)) + print + self.currentQuestion = q + + incompleted = self.prereqs[q].difference(completedQuestions) + if len(incompleted) > 0: + prereq = incompleted.pop() + print \ +"""*** NOTE: Make sure to complete Question %s before working on Question %s, +*** because Question %s builds upon your answer for Question %s. +""" % (prereq, q, q, prereq) + continue + + if self.mute: util.mutePrint() + try: + util.TimeoutFunction(getattr(gradingModule, q),1800)(self) # Call the question's function + #TimeoutFunction(getattr(gradingModule, q),1200)(self) # Call the question's function + except Exception, inst: + self.addExceptionMessage(q, inst, traceback) + self.addErrorHints(exceptionMap, inst, q[1]) + except: + self.fail('FAIL: Terminated with a string exception.') + finally: + if self.mute: util.unmutePrint() + + if self.points[q] >= self.maxes[q]: + completedQuestions.add(q) + + print '\n### Question %s: %d/%d ###\n' % (q, self.points[q], self.maxes[q]) + + + print '\nFinished at %d:%02d:%02d' % time.localtime()[3:6] + print "\nProvisional grades\n==================" + + for q in self.questions: + print 'Question %s: %d/%d' % (q, self.points[q], self.maxes[q]) + print '------------------' + print 'Total: %d/%d' % (self.points.totalCount(), sum(self.maxes.values())) + if bonusPic and self.points.totalCount() == 25: + print """ + + ALL HAIL GRANDPAC. + LONG LIVE THE GHOSTBUSTING KING. + + --- ---- --- + | \ / + \ / | + | + \--/ \--/ + | + | + + | + | + + + | + @@@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + \ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + \ / @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + V \ @@@@@@@@@@@@@@@@@@@@@@@@@@@@ + \ / @@@@@@@@@@@@@@@@@@@@@@@@@@ + V @@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@ + /\ @@@@@@@@@@@@@@@@@@@@@@ + / \ @@@@@@@@@@@@@@@@@@@@@@@@@ + /\ / @@@@@@@@@@@@@@@@@@@@@@@@@@@ + / \ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + / @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@@@@@@@@@@ + +""" + print """ +Your grades are NOT yet registered. To register your grades, make sure +to follow your instructor's guidelines to receive credit on your project. +""" + + if self.edxOutput: + self.produceOutput() + if self.gsOutput: + self.produceGradeScopeOutput() + + def addExceptionMessage(self, q, inst, traceback): + """ + Method to format the exception message, this is more complicated because + we need to cgi.escape the traceback but wrap the exception in a

 tag
+    """
+    self.fail('FAIL: Exception raised: %s' % inst)
+    self.addMessage('')
+    for line in traceback.format_exc().split('\n'):
+        self.addMessage(line)
+
+  def addErrorHints(self, exceptionMap, errorInstance, questionNum):
+    typeOf = str(type(errorInstance))
+    questionName = 'q' + questionNum
+    errorHint = ''
+
+    # question specific error hints
+    if exceptionMap.get(questionName):
+      questionMap = exceptionMap.get(questionName)
+      if (questionMap.get(typeOf)):
+        errorHint = questionMap.get(typeOf)
+    # fall back to general error messages if a question specific
+    # one does not exist
+    if (exceptionMap.get(typeOf)):
+      errorHint = exceptionMap.get(typeOf)
+
+    # dont include the HTML if we have no error hint
+    if not errorHint:
+      return ''
+
+    for line in errorHint.split('\n'):
+      self.addMessage(line)
+
+  def produceGradeScopeOutput(self):
+    out_dct = {}
+
+    # total of entire submission
+    total_possible = sum(self.maxes.values())
+    total_score = sum(self.points.values())
+    out_dct['score'] = total_score
+    out_dct['max_score'] = total_possible
+    out_dct['output'] = "Total score (%d / %d)" % (total_score, total_possible)
+
+    # individual tests
+    tests_out = []
+    for name in self.questions:
+      test_out = {}
+      # test name
+      test_out['name'] = name
+      # test score
+      test_out['score'] = self.points[name]
+      test_out['max_score'] = self.maxes[name]
+      # others
+      is_correct = self.points[name] >= self.maxes[name]
+      test_out['output'] = "  Question {num} ({points}/{max}) {correct}".format(
+          num=(name[1] if len(name) == 2 else name),
+          points=test_out['score'],
+          max=test_out['max_score'],
+          correct=('X' if not is_correct else ''),
+      )
+      test_out['tags'] = []
+      tests_out.append(test_out)
+    out_dct['tests'] = tests_out
+
+    # file output
+    with open('gradescope_response.json', 'w') as outfile:
+        json.dump(out_dct, outfile)
+    return
+
+  def produceOutput(self):
+    edxOutput = open('edx_response.html', 'w')
+    edxOutput.write("
") + + # first sum + total_possible = sum(self.maxes.values()) + total_score = sum(self.points.values()) + checkOrX = '' + if (total_score >= total_possible): + checkOrX = '' + header = """ +

+ Total score ({total_score} / {total_possible}) +

+ """.format(total_score = total_score, + total_possible = total_possible, + checkOrX = checkOrX + ) + edxOutput.write(header) + + for q in self.questions: + if len(q) == 2: + name = q[1] + else: + name = q + checkOrX = '' + if (self.points[q] >= self.maxes[q]): + checkOrX = '' + #messages = '\n
\n'.join(self.messages[q]) + messages = "
%s
" % '\n'.join(self.messages[q]) + output = """ +
+
+
+ Question {q} ({points}/{max}) {checkOrX} +
+
+ {messages} +
+
+
+ """.format(q = name, + max = self.maxes[q], + messages = messages, + checkOrX = checkOrX, + points = self.points[q] + ) + # print "*** output for Question %s " % q[1] + # print output + edxOutput.write(output) + edxOutput.write("
") + edxOutput.close() + edxOutput = open('edx_grade', 'w') + edxOutput.write(str(self.points.totalCount())) + edxOutput.close() + + def fail(self, message, raw=False): + "Sets sanity check bit to false and outputs a message" + self.sane = False + self.assignZeroCredit() + self.addMessage(message, raw) + + def assignZeroCredit(self): + self.points[self.currentQuestion] = 0 + + def addPoints(self, amt): + self.points[self.currentQuestion] += amt + + def deductPoints(self, amt): + self.points[self.currentQuestion] -= amt + + def assignFullCredit(self, message="", raw=False): + self.points[self.currentQuestion] = self.maxes[self.currentQuestion] + if message != "": + self.addMessage(message, raw) + + def addMessage(self, message, raw=False): + if not raw: + # We assume raw messages, formatted for HTML, are printed separately + if self.mute: util.unmutePrint() + print '*** ' + message + if self.mute: util.mutePrint() + message = cgi.escape(message) + self.messages[self.currentQuestion].append(message) + + def addMessageToEmail(self, message): + print "WARNING**** addMessageToEmail is deprecated %s" % message + for line in message.split('\n'): + pass + #print '%%% ' + line + ' %%%' + #self.messages[self.currentQuestion].append(line) + + + + + +class Counter(dict): + """ + Dict with default 0 + """ + def __getitem__(self, idx): + try: + return dict.__getitem__(self, idx) + except KeyError: + return 0 + + def totalCount(self): + """ + Returns the sum of counts for all keys. + """ + return sum(self.values()) + diff --git a/graphicsDisplay.py b/graphicsDisplay.py new file mode 100644 index 0000000..1bfe1b3 --- /dev/null +++ b/graphicsDisplay.py @@ -0,0 +1,679 @@ +# graphicsDisplay.py +# ------------------ +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). + + +from graphicsUtils import * +import math, time +from game import Directions + +########################### +# GRAPHICS DISPLAY CODE # +########################### + +# Most code by Dan Klein and John Denero written or rewritten for cs188, UC Berkeley. +# Some code from a Pacman implementation by LiveWires, and used / modified with permission. + +DEFAULT_GRID_SIZE = 30.0 +INFO_PANE_HEIGHT = 35 +BACKGROUND_COLOR = formatColor(0,0,0) +WALL_COLOR = formatColor(0.0/255.0, 51.0/255.0, 255.0/255.0) +INFO_PANE_COLOR = formatColor(.4,.4,0) +SCORE_COLOR = formatColor(.9, .9, .9) +PACMAN_OUTLINE_WIDTH = 2 +PACMAN_CAPTURE_OUTLINE_WIDTH = 4 + +GHOST_COLORS = [] +GHOST_COLORS.append(formatColor(.9,0,0)) # Red +GHOST_COLORS.append(formatColor(0,.3,.9)) # Blue +GHOST_COLORS.append(formatColor(.98,.41,.07)) # Orange +GHOST_COLORS.append(formatColor(.1,.75,.7)) # Green +GHOST_COLORS.append(formatColor(1.0,0.6,0.0)) # Yellow +GHOST_COLORS.append(formatColor(.4,0.13,0.91)) # Purple + +TEAM_COLORS = GHOST_COLORS[:2] + +GHOST_SHAPE = [ + ( 0, 0.3 ), + ( 0.25, 0.75 ), + ( 0.5, 0.3 ), + ( 0.75, 0.75 ), + ( 0.75, -0.5 ), + ( 0.5, -0.75 ), + (-0.5, -0.75 ), + (-0.75, -0.5 ), + (-0.75, 0.75 ), + (-0.5, 0.3 ), + (-0.25, 0.75 ) + ] +GHOST_SIZE = 0.65 +SCARED_COLOR = formatColor(1,1,1) + +GHOST_VEC_COLORS = map(colorToVector, GHOST_COLORS) + +PACMAN_COLOR = formatColor(255.0/255.0,255.0/255.0,61.0/255) +PACMAN_SCALE = 0.5 +#pacman_speed = 0.25 + +# Food +FOOD_COLOR = formatColor(1,1,1) +FOOD_SIZE = 0.1 + +# Laser +LASER_COLOR = formatColor(1,0,0) +LASER_SIZE = 0.02 + +# Capsule graphics +CAPSULE_COLOR = formatColor(1,1,1) +CAPSULE_SIZE = 0.25 + +# Drawing walls +WALL_RADIUS = 0.15 + +class InfoPane: + def __init__(self, layout, gridSize): + self.gridSize = gridSize + self.width = (layout.width) * gridSize + self.base = (layout.height + 1) * gridSize + self.height = INFO_PANE_HEIGHT + self.fontSize = 24 + self.textColor = PACMAN_COLOR + self.drawPane() + + def toScreen(self, pos, y = None): + """ + Translates a point relative from the bottom left of the info pane. + """ + if y == None: + x,y = pos + else: + x = pos + + x = self.gridSize + x # Margin + y = self.base + y + return x,y + + def drawPane(self): + self.scoreText = text( self.toScreen(0, 0 ), self.textColor, "SCORE: 0", "Times", self.fontSize, "bold") + + def initializeGhostDistances(self, distances): + self.ghostDistanceText = [] + + size = 20 + if self.width < 240: + size = 12 + if self.width < 160: + size = 10 + + for i, d in enumerate(distances): + t = text( self.toScreen(self.width/2 + self.width/8 * i, 0), GHOST_COLORS[i+1], d, "Times", size, "bold") + self.ghostDistanceText.append(t) + + def updateScore(self, score): + changeText(self.scoreText, "SCORE: % 4d" % score) + + def setTeam(self, isBlue): + text = "RED TEAM" + if isBlue: text = "BLUE TEAM" + self.teamText = text( self.toScreen(300, 0 ), self.textColor, text, "Times", self.fontSize, "bold") + + def updateGhostDistances(self, distances): + if len(distances) == 0: return + if 'ghostDistanceText' not in dir(self): self.initializeGhostDistances(distances) + else: + for i, d in enumerate(distances): + changeText(self.ghostDistanceText[i], d) + + def drawGhost(self): + pass + + def drawPacman(self): + pass + + def drawWarning(self): + pass + + def clearIcon(self): + pass + + def updateMessage(self, message): + pass + + def clearMessage(self): + pass + + +class PacmanGraphics: + def __init__(self, zoom=1.0, frameTime=0.0, capture=False): + self.have_window = 0 + self.currentGhostImages = {} + self.pacmanImage = None + self.zoom = zoom + self.gridSize = DEFAULT_GRID_SIZE * zoom + self.capture = capture + self.frameTime = frameTime + + def checkNullDisplay(self): + return False + + def initialize(self, state, isBlue = False): + self.isBlue = isBlue + self.startGraphics(state) + + # self.drawDistributions(state) + self.distributionImages = None # Initialized lazily + self.drawStaticObjects(state) + self.drawAgentObjects(state) + + # Information + self.previousState = state + + def startGraphics(self, state): + self.layout = state.layout + layout = self.layout + self.width = layout.width + self.height = layout.height + self.make_window(self.width, self.height) + self.infoPane = InfoPane(layout, self.gridSize) + self.currentState = layout + + def drawDistributions(self, state): + walls = state.layout.walls + dist = [] + for x in range(walls.width): + distx = [] + dist.append(distx) + for y in range(walls.height): + ( screen_x, screen_y ) = self.to_screen( (x, y) ) + block = square( (screen_x, screen_y), + 0.5 * self.gridSize, + color = BACKGROUND_COLOR, + filled = 1, behind=2) + distx.append(block) + self.distributionImages = dist + + def drawStaticObjects(self, state): + layout = self.layout + self.drawWalls(layout.walls) + self.food = self.drawFood(layout.food) + self.capsules = self.drawCapsules(layout.capsules) + refresh() + + def drawAgentObjects(self, state): + self.agentImages = [] # (agentState, image) + for index, agent in enumerate(state.agentStates): + if agent.isPacman: + image = self.drawPacman(agent, index) + self.agentImages.append( (agent, image) ) + else: + image = self.drawGhost(agent, index) + self.agentImages.append( (agent, image) ) + refresh() + + def swapImages(self, agentIndex, newState): + """ + Changes an image from a ghost to a pacman or vis versa (for capture) + """ + prevState, prevImage = self.agentImages[agentIndex] + for item in prevImage: remove_from_screen(item) + if newState.isPacman: + image = self.drawPacman(newState, agentIndex) + self.agentImages[agentIndex] = (newState, image ) + else: + image = self.drawGhost(newState, agentIndex) + self.agentImages[agentIndex] = (newState, image ) + refresh() + + def update(self, newState): + agentIndex = newState._agentMoved + agentState = newState.agentStates[agentIndex] + + if self.agentImages[agentIndex][0].isPacman != agentState.isPacman: self.swapImages(agentIndex, agentState) + prevState, prevImage = self.agentImages[agentIndex] + if agentState.isPacman: + self.animatePacman(agentState, prevState, prevImage) + else: + self.moveGhost(agentState, agentIndex, prevState, prevImage) + self.agentImages[agentIndex] = (agentState, prevImage) + + if newState._foodEaten != None: + self.removeFood(newState._foodEaten, self.food) + if newState._capsuleEaten != None: + self.removeCapsule(newState._capsuleEaten, self.capsules) + self.infoPane.updateScore(newState.score) + if 'ghostDistances' in dir(newState): + self.infoPane.updateGhostDistances(newState.ghostDistances) + + def make_window(self, width, height): + grid_width = (width-1) * self.gridSize + grid_height = (height-1) * self.gridSize + screen_width = 2*self.gridSize + grid_width + screen_height = 2*self.gridSize + grid_height + INFO_PANE_HEIGHT + + begin_graphics(screen_width, + screen_height, + BACKGROUND_COLOR, + "CS188 Pacman") + + def drawPacman(self, pacman, index): + position = self.getPosition(pacman) + screen_point = self.to_screen(position) + endpoints = self.getEndpoints(self.getDirection(pacman)) + + width = PACMAN_OUTLINE_WIDTH + outlineColor = PACMAN_COLOR + fillColor = PACMAN_COLOR + + if self.capture: + outlineColor = TEAM_COLORS[index % 2] + fillColor = GHOST_COLORS[index] + width = PACMAN_CAPTURE_OUTLINE_WIDTH + + return [circle(screen_point, PACMAN_SCALE * self.gridSize, + fillColor = fillColor, outlineColor = outlineColor, + endpoints = endpoints, + width = width)] + + def getEndpoints(self, direction, position=(0,0)): + x, y = position + pos = x - int(x) + y - int(y) + width = 30 + 80 * math.sin(math.pi* pos) + + delta = width / 2 + if (direction == 'West'): + endpoints = (180+delta, 180-delta) + elif (direction == 'North'): + endpoints = (90+delta, 90-delta) + elif (direction == 'South'): + endpoints = (270+delta, 270-delta) + else: + endpoints = (0+delta, 0-delta) + return endpoints + + def movePacman(self, position, direction, image): + screenPosition = self.to_screen(position) + endpoints = self.getEndpoints( direction, position ) + r = PACMAN_SCALE * self.gridSize + moveCircle(image[0], screenPosition, r, endpoints) + refresh() + + def animatePacman(self, pacman, prevPacman, image): + if self.frameTime < 0: + print 'Press any key to step forward, "q" to play' + keys = wait_for_keys() + if 'q' in keys: + self.frameTime = 0.1 + if self.frameTime > 0.01 or self.frameTime < 0: + start = time.time() + fx, fy = self.getPosition(prevPacman) + px, py = self.getPosition(pacman) + frames = 4.0 + for i in range(1,int(frames) + 1): + pos = px*i/frames + fx*(frames-i)/frames, py*i/frames + fy*(frames-i)/frames + self.movePacman(pos, self.getDirection(pacman), image) + refresh() + sleep(abs(self.frameTime) / frames) + else: + self.movePacman(self.getPosition(pacman), self.getDirection(pacman), image) + refresh() + + def getGhostColor(self, ghost, ghostIndex): + if ghost.scaredTimer > 0: + return SCARED_COLOR + else: + return GHOST_COLORS[ghostIndex] + + def drawGhost(self, ghost, agentIndex): + pos = self.getPosition(ghost) + dir = self.getDirection(ghost) + (screen_x, screen_y) = (self.to_screen(pos) ) + coords = [] + for (x, y) in GHOST_SHAPE: + coords.append((x*self.gridSize*GHOST_SIZE + screen_x, y*self.gridSize*GHOST_SIZE + screen_y)) + + colour = self.getGhostColor(ghost, agentIndex) + body = polygon(coords, colour, filled = 1) + WHITE = formatColor(1.0, 1.0, 1.0) + BLACK = formatColor(0.0, 0.0, 0.0) + + dx = 0 + dy = 0 + if dir == 'North': + dy = -0.2 + if dir == 'South': + dy = 0.2 + if dir == 'East': + dx = 0.2 + if dir == 'West': + dx = -0.2 + leftEye = circle((screen_x+self.gridSize*GHOST_SIZE*(-0.3+dx/1.5), screen_y-self.gridSize*GHOST_SIZE*(0.3-dy/1.5)), self.gridSize*GHOST_SIZE*0.2, WHITE, WHITE) + rightEye = circle((screen_x+self.gridSize*GHOST_SIZE*(0.3+dx/1.5), screen_y-self.gridSize*GHOST_SIZE*(0.3-dy/1.5)), self.gridSize*GHOST_SIZE*0.2, WHITE, WHITE) + leftPupil = circle((screen_x+self.gridSize*GHOST_SIZE*(-0.3+dx), screen_y-self.gridSize*GHOST_SIZE*(0.3-dy)), self.gridSize*GHOST_SIZE*0.08, BLACK, BLACK) + rightPupil = circle((screen_x+self.gridSize*GHOST_SIZE*(0.3+dx), screen_y-self.gridSize*GHOST_SIZE*(0.3-dy)), self.gridSize*GHOST_SIZE*0.08, BLACK, BLACK) + ghostImageParts = [] + ghostImageParts.append(body) + ghostImageParts.append(leftEye) + ghostImageParts.append(rightEye) + ghostImageParts.append(leftPupil) + ghostImageParts.append(rightPupil) + + return ghostImageParts + + def moveEyes(self, pos, dir, eyes): + (screen_x, screen_y) = (self.to_screen(pos) ) + dx = 0 + dy = 0 + if dir == 'North': + dy = -0.2 + if dir == 'South': + dy = 0.2 + if dir == 'East': + dx = 0.2 + if dir == 'West': + dx = -0.2 + moveCircle(eyes[0],(screen_x+self.gridSize*GHOST_SIZE*(-0.3+dx/1.5), screen_y-self.gridSize*GHOST_SIZE*(0.3-dy/1.5)), self.gridSize*GHOST_SIZE*0.2) + moveCircle(eyes[1],(screen_x+self.gridSize*GHOST_SIZE*(0.3+dx/1.5), screen_y-self.gridSize*GHOST_SIZE*(0.3-dy/1.5)), self.gridSize*GHOST_SIZE*0.2) + moveCircle(eyes[2],(screen_x+self.gridSize*GHOST_SIZE*(-0.3+dx), screen_y-self.gridSize*GHOST_SIZE*(0.3-dy)), self.gridSize*GHOST_SIZE*0.08) + moveCircle(eyes[3],(screen_x+self.gridSize*GHOST_SIZE*(0.3+dx), screen_y-self.gridSize*GHOST_SIZE*(0.3-dy)), self.gridSize*GHOST_SIZE*0.08) + + def moveGhost(self, ghost, ghostIndex, prevGhost, ghostImageParts): + old_x, old_y = self.to_screen(self.getPosition(prevGhost)) + new_x, new_y = self.to_screen(self.getPosition(ghost)) + delta = new_x - old_x, new_y - old_y + + for ghostImagePart in ghostImageParts: + move_by(ghostImagePart, delta) + refresh() + + if ghost.scaredTimer > 0: + color = SCARED_COLOR + else: + color = GHOST_COLORS[ghostIndex] + edit(ghostImageParts[0], ('fill', color), ('outline', color)) + self.moveEyes(self.getPosition(ghost), self.getDirection(ghost), ghostImageParts[-4:]) + refresh() + + def getPosition(self, agentState): + if agentState.configuration == None: return (-1000, -1000) + return agentState.getPosition() + + def getDirection(self, agentState): + if agentState.configuration == None: return Directions.STOP + return agentState.configuration.getDirection() + + def finish(self): + end_graphics() + + def to_screen(self, point): + ( x, y ) = point + #y = self.height - y + x = (x + 1)*self.gridSize + y = (self.height - y)*self.gridSize + return ( x, y ) + + # Fixes some TK issue with off-center circles + def to_screen2(self, point): + ( x, y ) = point + #y = self.height - y + x = (x + 1)*self.gridSize + y = (self.height - y)*self.gridSize + return ( x, y ) + + def drawWalls(self, wallMatrix): + wallColor = WALL_COLOR + for xNum, x in enumerate(wallMatrix): + if self.capture and (xNum * 2) < wallMatrix.width: wallColor = TEAM_COLORS[0] + if self.capture and (xNum * 2) >= wallMatrix.width: wallColor = TEAM_COLORS[1] + + for yNum, cell in enumerate(x): + if cell: # There's a wall here + pos = (xNum, yNum) + screen = self.to_screen(pos) + screen2 = self.to_screen2(pos) + + # draw each quadrant of the square based on adjacent walls + wIsWall = self.isWall(xNum-1, yNum, wallMatrix) + eIsWall = self.isWall(xNum+1, yNum, wallMatrix) + nIsWall = self.isWall(xNum, yNum+1, wallMatrix) + sIsWall = self.isWall(xNum, yNum-1, wallMatrix) + nwIsWall = self.isWall(xNum-1, yNum+1, wallMatrix) + swIsWall = self.isWall(xNum-1, yNum-1, wallMatrix) + neIsWall = self.isWall(xNum+1, yNum+1, wallMatrix) + seIsWall = self.isWall(xNum+1, yNum-1, wallMatrix) + + # NE quadrant + if (not nIsWall) and (not eIsWall): + # inner circle + circle(screen2, WALL_RADIUS * self.gridSize, wallColor, wallColor, (0,91), 'arc') + if (nIsWall) and (not eIsWall): + # vertical line + line(add(screen, (self.gridSize*WALL_RADIUS, 0)), add(screen, (self.gridSize*WALL_RADIUS, self.gridSize*(-0.5)-1)), wallColor) + if (not nIsWall) and (eIsWall): + # horizontal line + line(add(screen, (0, self.gridSize*(-1)*WALL_RADIUS)), add(screen, (self.gridSize*0.5+1, self.gridSize*(-1)*WALL_RADIUS)), wallColor) + if (nIsWall) and (eIsWall) and (not neIsWall): + # outer circle + circle(add(screen2, (self.gridSize*2*WALL_RADIUS, self.gridSize*(-2)*WALL_RADIUS)), WALL_RADIUS * self.gridSize-1, wallColor, wallColor, (180,271), 'arc') + line(add(screen, (self.gridSize*2*WALL_RADIUS-1, self.gridSize*(-1)*WALL_RADIUS)), add(screen, (self.gridSize*0.5+1, self.gridSize*(-1)*WALL_RADIUS)), wallColor) + line(add(screen, (self.gridSize*WALL_RADIUS, self.gridSize*(-2)*WALL_RADIUS+1)), add(screen, (self.gridSize*WALL_RADIUS, self.gridSize*(-0.5))), wallColor) + + # NW quadrant + if (not nIsWall) and (not wIsWall): + # inner circle + circle(screen2, WALL_RADIUS * self.gridSize, wallColor, wallColor, (90,181), 'arc') + if (nIsWall) and (not wIsWall): + # vertical line + line(add(screen, (self.gridSize*(-1)*WALL_RADIUS, 0)), add(screen, (self.gridSize*(-1)*WALL_RADIUS, self.gridSize*(-0.5)-1)), wallColor) + if (not nIsWall) and (wIsWall): + # horizontal line + line(add(screen, (0, self.gridSize*(-1)*WALL_RADIUS)), add(screen, (self.gridSize*(-0.5)-1, self.gridSize*(-1)*WALL_RADIUS)), wallColor) + if (nIsWall) and (wIsWall) and (not nwIsWall): + # outer circle + circle(add(screen2, (self.gridSize*(-2)*WALL_RADIUS, self.gridSize*(-2)*WALL_RADIUS)), WALL_RADIUS * self.gridSize-1, wallColor, wallColor, (270,361), 'arc') + line(add(screen, (self.gridSize*(-2)*WALL_RADIUS+1, self.gridSize*(-1)*WALL_RADIUS)), add(screen, (self.gridSize*(-0.5), self.gridSize*(-1)*WALL_RADIUS)), wallColor) + line(add(screen, (self.gridSize*(-1)*WALL_RADIUS, self.gridSize*(-2)*WALL_RADIUS+1)), add(screen, (self.gridSize*(-1)*WALL_RADIUS, self.gridSize*(-0.5))), wallColor) + + # SE quadrant + if (not sIsWall) and (not eIsWall): + # inner circle + circle(screen2, WALL_RADIUS * self.gridSize, wallColor, wallColor, (270,361), 'arc') + if (sIsWall) and (not eIsWall): + # vertical line + line(add(screen, (self.gridSize*WALL_RADIUS, 0)), add(screen, (self.gridSize*WALL_RADIUS, self.gridSize*(0.5)+1)), wallColor) + if (not sIsWall) and (eIsWall): + # horizontal line + line(add(screen, (0, self.gridSize*(1)*WALL_RADIUS)), add(screen, (self.gridSize*0.5+1, self.gridSize*(1)*WALL_RADIUS)), wallColor) + if (sIsWall) and (eIsWall) and (not seIsWall): + # outer circle + circle(add(screen2, (self.gridSize*2*WALL_RADIUS, self.gridSize*(2)*WALL_RADIUS)), WALL_RADIUS * self.gridSize-1, wallColor, wallColor, (90,181), 'arc') + line(add(screen, (self.gridSize*2*WALL_RADIUS-1, self.gridSize*(1)*WALL_RADIUS)), add(screen, (self.gridSize*0.5, self.gridSize*(1)*WALL_RADIUS)), wallColor) + line(add(screen, (self.gridSize*WALL_RADIUS, self.gridSize*(2)*WALL_RADIUS-1)), add(screen, (self.gridSize*WALL_RADIUS, self.gridSize*(0.5))), wallColor) + + # SW quadrant + if (not sIsWall) and (not wIsWall): + # inner circle + circle(screen2, WALL_RADIUS * self.gridSize, wallColor, wallColor, (180,271), 'arc') + if (sIsWall) and (not wIsWall): + # vertical line + line(add(screen, (self.gridSize*(-1)*WALL_RADIUS, 0)), add(screen, (self.gridSize*(-1)*WALL_RADIUS, self.gridSize*(0.5)+1)), wallColor) + if (not sIsWall) and (wIsWall): + # horizontal line + line(add(screen, (0, self.gridSize*(1)*WALL_RADIUS)), add(screen, (self.gridSize*(-0.5)-1, self.gridSize*(1)*WALL_RADIUS)), wallColor) + if (sIsWall) and (wIsWall) and (not swIsWall): + # outer circle + circle(add(screen2, (self.gridSize*(-2)*WALL_RADIUS, self.gridSize*(2)*WALL_RADIUS)), WALL_RADIUS * self.gridSize-1, wallColor, wallColor, (0,91), 'arc') + line(add(screen, (self.gridSize*(-2)*WALL_RADIUS+1, self.gridSize*(1)*WALL_RADIUS)), add(screen, (self.gridSize*(-0.5), self.gridSize*(1)*WALL_RADIUS)), wallColor) + line(add(screen, (self.gridSize*(-1)*WALL_RADIUS, self.gridSize*(2)*WALL_RADIUS-1)), add(screen, (self.gridSize*(-1)*WALL_RADIUS, self.gridSize*(0.5))), wallColor) + + def isWall(self, x, y, walls): + if x < 0 or y < 0: + return False + if x >= walls.width or y >= walls.height: + return False + return walls[x][y] + + def drawFood(self, foodMatrix ): + foodImages = [] + color = FOOD_COLOR + for xNum, x in enumerate(foodMatrix): + if self.capture and (xNum * 2) <= foodMatrix.width: color = TEAM_COLORS[0] + if self.capture and (xNum * 2) > foodMatrix.width: color = TEAM_COLORS[1] + imageRow = [] + foodImages.append(imageRow) + for yNum, cell in enumerate(x): + if cell: # There's food here + screen = self.to_screen((xNum, yNum )) + dot = circle( screen, + FOOD_SIZE * self.gridSize, + outlineColor = color, fillColor = color, + width = 1) + imageRow.append(dot) + else: + imageRow.append(None) + return foodImages + + def drawCapsules(self, capsules ): + capsuleImages = {} + for capsule in capsules: + ( screen_x, screen_y ) = self.to_screen(capsule) + dot = circle( (screen_x, screen_y), + CAPSULE_SIZE * self.gridSize, + outlineColor = CAPSULE_COLOR, + fillColor = CAPSULE_COLOR, + width = 1) + capsuleImages[capsule] = dot + return capsuleImages + + def removeFood(self, cell, foodImages ): + x, y = cell + remove_from_screen(foodImages[x][y]) + + def removeCapsule(self, cell, capsuleImages ): + x, y = cell + remove_from_screen(capsuleImages[(x, y)]) + + def drawExpandedCells(self, cells): + """ + Draws an overlay of expanded grid positions for search agents + """ + n = float(len(cells)) + baseColor = [1.0, 0.0, 0.0] + self.clearExpandedCells() + self.expandedCells = [] + for k, cell in enumerate(cells): + screenPos = self.to_screen( cell) + cellColor = formatColor(*[(n-k) * c * .5 / n + .25 for c in baseColor]) + block = square(screenPos, + 0.5 * self.gridSize, + color = cellColor, + filled = 1, behind=2) + self.expandedCells.append(block) + if self.frameTime < 0: + refresh() + + def clearExpandedCells(self): + if 'expandedCells' in dir(self) and len(self.expandedCells) > 0: + for cell in self.expandedCells: + remove_from_screen(cell) + + + def updateDistributions(self, distributions): + "Draws an agent's belief distributions" + # copy all distributions so we don't change their state + distributions = map(lambda x: x.copy(), distributions) + if self.distributionImages == None: + self.drawDistributions(self.previousState) + for x in range(len(self.distributionImages)): + for y in range(len(self.distributionImages[0])): + image = self.distributionImages[x][y] + weights = [dist[ (x,y) ] for dist in distributions] + + if sum(weights) != 0: + pass + # Fog of war + color = [0.0,0.0,0.0] + colors = GHOST_VEC_COLORS[1:] # With Pacman + if self.capture: colors = GHOST_VEC_COLORS + for weight, gcolor in zip(weights, colors): + color = [min(1.0, c + 0.95 * g * weight ** .3) for c,g in zip(color, gcolor)] + changeColor(image, formatColor(*color)) + refresh() + +class FirstPersonPacmanGraphics(PacmanGraphics): + def __init__(self, zoom = 1.0, showGhosts = True, capture = False, frameTime=0): + PacmanGraphics.__init__(self, zoom, frameTime=frameTime) + self.showGhosts = showGhosts + self.capture = capture + + def initialize(self, state, isBlue = False): + + self.isBlue = isBlue + PacmanGraphics.startGraphics(self, state) + # Initialize distribution images + walls = state.layout.walls + dist = [] + self.layout = state.layout + + # Draw the rest + self.distributionImages = None # initialize lazily + self.drawStaticObjects(state) + self.drawAgentObjects(state) + + # Information + self.previousState = state + + def lookAhead(self, config, state): + if config.getDirection() == 'Stop': + return + else: + pass + # Draw relevant ghosts + allGhosts = state.getGhostStates() + visibleGhosts = state.getVisibleGhosts() + for i, ghost in enumerate(allGhosts): + if ghost in visibleGhosts: + self.drawGhost(ghost, i) + else: + self.currentGhostImages[i] = None + + def getGhostColor(self, ghost, ghostIndex): + return GHOST_COLORS[ghostIndex] + + def getPosition(self, ghostState): + if not self.showGhosts and not ghostState.isPacman and ghostState.getPosition()[1] > 1: + return (-1000, -1000) + else: + return PacmanGraphics.getPosition(self, ghostState) + +def add(x, y): + return (x[0] + y[0], x[1] + y[1]) + + +# Saving graphical output +# ----------------------- +# Note: to make an animated gif from this postscript output, try the command: +# convert -delay 7 -loop 1 -compress lzw -layers optimize frame* out.gif +# convert is part of imagemagick (freeware) + +SAVE_POSTSCRIPT = False +POSTSCRIPT_OUTPUT_DIR = 'frames' +FRAME_NUMBER = 0 +import os + +def saveFrame(): + "Saves the current graphical output as a postscript file" + global SAVE_POSTSCRIPT, FRAME_NUMBER, POSTSCRIPT_OUTPUT_DIR + if not SAVE_POSTSCRIPT: return + if not os.path.exists(POSTSCRIPT_OUTPUT_DIR): os.mkdir(POSTSCRIPT_OUTPUT_DIR) + name = os.path.join(POSTSCRIPT_OUTPUT_DIR, 'frame_%08d.ps' % FRAME_NUMBER) + FRAME_NUMBER += 1 + writePostscript(name) # writes the current canvas diff --git a/graphicsUtils.py b/graphicsUtils.py new file mode 100644 index 0000000..b80d3d2 --- /dev/null +++ b/graphicsUtils.py @@ -0,0 +1,402 @@ +# graphicsUtils.py +# ---------------- +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). + + +import sys +import math +import random +import string +import time +import types +import Tkinter +import os.path + +_Windows = sys.platform == 'win32' # True if on Win95/98/NT + +_root_window = None # The root window for graphics output +_canvas = None # The canvas which holds graphics +_canvas_xs = None # Size of canvas object +_canvas_ys = None +_canvas_x = None # Current position on canvas +_canvas_y = None +_canvas_col = None # Current colour (set to black below) +_canvas_tsize = 12 +_canvas_tserifs = 0 + +def formatColor(r, g, b): + return '#%02x%02x%02x' % (int(r * 255), int(g * 255), int(b * 255)) + +def colorToVector(color): + return map(lambda x: int(x, 16) / 256.0, [color[1:3], color[3:5], color[5:7]]) + +if _Windows: + _canvas_tfonts = ['times new roman', 'lucida console'] +else: + _canvas_tfonts = ['times', 'lucidasans-24'] + pass # XXX need defaults here + +def sleep(secs): + global _root_window + if _root_window == None: + time.sleep(secs) + else: + _root_window.update_idletasks() + _root_window.after(int(1000 * secs), _root_window.quit) + _root_window.mainloop() + +def begin_graphics(width=640, height=480, color=formatColor(0, 0, 0), title=None): + + global _root_window, _canvas, _canvas_x, _canvas_y, _canvas_xs, _canvas_ys, _bg_color + + # Check for duplicate call + if _root_window is not None: + # Lose the window. + _root_window.destroy() + + # Save the canvas size parameters + _canvas_xs, _canvas_ys = width - 1, height - 1 + _canvas_x, _canvas_y = 0, _canvas_ys + _bg_color = color + + # Create the root window + _root_window = Tkinter.Tk() + _root_window.protocol('WM_DELETE_WINDOW', _destroy_window) + _root_window.title(title or 'Graphics Window') + _root_window.resizable(0, 0) + + # Create the canvas object + try: + _canvas = Tkinter.Canvas(_root_window, width=width, height=height) + _canvas.pack() + draw_background() + _canvas.update() + except: + _root_window = None + raise + + # Bind to key-down and key-up events + _root_window.bind( "", _keypress ) + _root_window.bind( "", _keyrelease ) + _root_window.bind( "", _clear_keys ) + _root_window.bind( "", _clear_keys ) + _root_window.bind( "", _leftclick ) + _root_window.bind( "", _rightclick ) + _root_window.bind( "", _rightclick ) + _root_window.bind( "", _ctrl_leftclick) + _clear_keys() + +_leftclick_loc = None +_rightclick_loc = None +_ctrl_leftclick_loc = None + +def _leftclick(event): + global _leftclick_loc + _leftclick_loc = (event.x, event.y) + +def _rightclick(event): + global _rightclick_loc + _rightclick_loc = (event.x, event.y) + +def _ctrl_leftclick(event): + global _ctrl_leftclick_loc + _ctrl_leftclick_loc = (event.x, event.y) + +def wait_for_click(): + while True: + global _leftclick_loc + global _rightclick_loc + global _ctrl_leftclick_loc + if _leftclick_loc != None: + val = _leftclick_loc + _leftclick_loc = None + return val, 'left' + if _rightclick_loc != None: + val = _rightclick_loc + _rightclick_loc = None + return val, 'right' + if _ctrl_leftclick_loc != None: + val = _ctrl_leftclick_loc + _ctrl_leftclick_loc = None + return val, 'ctrl_left' + sleep(0.05) + +def draw_background(): + corners = [(0,0), (0, _canvas_ys), (_canvas_xs, _canvas_ys), (_canvas_xs, 0)] + polygon(corners, _bg_color, fillColor=_bg_color, filled=True, smoothed=False) + +def _destroy_window(event=None): + sys.exit(0) +# global _root_window +# _root_window.destroy() +# _root_window = None + #print "DESTROY" + +def end_graphics(): + global _root_window, _canvas, _mouse_enabled + try: + try: + sleep(1) + if _root_window != None: + _root_window.destroy() + except SystemExit, e: + print 'Ending graphics raised an exception:', e + finally: + _root_window = None + _canvas = None + _mouse_enabled = 0 + _clear_keys() + +def clear_screen(background=None): + global _canvas_x, _canvas_y + _canvas.delete('all') + draw_background() + _canvas_x, _canvas_y = 0, _canvas_ys + +def polygon(coords, outlineColor, fillColor=None, filled=1, smoothed=1, behind=0, width=1): + c = [] + for coord in coords: + c.append(coord[0]) + c.append(coord[1]) + if fillColor == None: fillColor = outlineColor + if filled == 0: fillColor = "" + poly = _canvas.create_polygon(c, outline=outlineColor, fill=fillColor, smooth=smoothed, width=width) + if behind > 0: + _canvas.tag_lower(poly, behind) # Higher should be more visible + return poly + +def square(pos, r, color, filled=1, behind=0): + x, y = pos + coords = [(x - r, y - r), (x + r, y - r), (x + r, y + r), (x - r, y + r)] + return polygon(coords, color, color, filled, 0, behind=behind) + +def circle(pos, r, outlineColor, fillColor, endpoints=None, style='pieslice', width=2): + x, y = pos + x0, x1 = x - r - 1, x + r + y0, y1 = y - r - 1, y + r + if endpoints == None: + e = [0, 359] + else: + e = list(endpoints) + while e[0] > e[1]: e[1] = e[1] + 360 + + return _canvas.create_arc(x0, y0, x1, y1, outline=outlineColor, fill=fillColor, + extent=e[1] - e[0], start=e[0], style=style, width=width) + +def image(pos, file="../../blueghost.gif"): + x, y = pos + # img = PhotoImage(file=file) + return _canvas.create_image(x, y, image = Tkinter.PhotoImage(file=file), anchor = Tkinter.NW) + + +def refresh(): + _canvas.update_idletasks() + +def moveCircle(id, pos, r, endpoints=None): + global _canvas_x, _canvas_y + + x, y = pos +# x0, x1 = x - r, x + r + 1 +# y0, y1 = y - r, y + r + 1 + x0, x1 = x - r - 1, x + r + y0, y1 = y - r - 1, y + r + if endpoints == None: + e = [0, 359] + else: + e = list(endpoints) + while e[0] > e[1]: e[1] = e[1] + 360 + + if os.path.isfile('flag'): + edit(id, ('extent', e[1] - e[0])) + else: + edit(id, ('start', e[0]), ('extent', e[1] - e[0])) + move_to(id, x0, y0) + +def edit(id, *args): + _canvas.itemconfigure(id, **dict(args)) + +def text(pos, color, contents, font='Helvetica', size=12, style='normal', anchor="nw"): + global _canvas_x, _canvas_y + x, y = pos + font = (font, str(size), style) + return _canvas.create_text(x, y, fill=color, text=contents, font=font, anchor=anchor) + +def changeText(id, newText, font=None, size=12, style='normal'): + _canvas.itemconfigure(id, text=newText) + if font != None: + _canvas.itemconfigure(id, font=(font, '-%d' % size, style)) + +def changeColor(id, newColor): + _canvas.itemconfigure(id, fill=newColor) + +def line(here, there, color=formatColor(0, 0, 0), width=2): + x0, y0 = here[0], here[1] + x1, y1 = there[0], there[1] + return _canvas.create_line(x0, y0, x1, y1, fill=color, width=width) + +############################################################################## +### Keypress handling ######################################################## +############################################################################## + +# We bind to key-down and key-up events. + +_keysdown = {} +_keyswaiting = {} +# This holds an unprocessed key release. We delay key releases by up to +# one call to keys_pressed() to get round a problem with auto repeat. +_got_release = None + +def _keypress(event): + global _got_release + #remap_arrows(event) + _keysdown[event.keysym] = 1 + _keyswaiting[event.keysym] = 1 +# print event.char, event.keycode + _got_release = None + +def _keyrelease(event): + global _got_release + #remap_arrows(event) + try: + del _keysdown[event.keysym] + except: + pass + _got_release = 1 + +def remap_arrows(event): + # TURN ARROW PRESSES INTO LETTERS (SHOULD BE IN KEYBOARD AGENT) + if event.char in ['a', 's', 'd', 'w']: + return + if event.keycode in [37, 101]: # LEFT ARROW (win / x) + event.char = 'a' + if event.keycode in [38, 99]: # UP ARROW + event.char = 'w' + if event.keycode in [39, 102]: # RIGHT ARROW + event.char = 'd' + if event.keycode in [40, 104]: # DOWN ARROW + event.char = 's' + +def _clear_keys(event=None): + global _keysdown, _got_release, _keyswaiting + _keysdown = {} + _keyswaiting = {} + _got_release = None + +def keys_pressed(d_o_e=Tkinter.tkinter.dooneevent, + d_w=Tkinter.tkinter.DONT_WAIT): + d_o_e(d_w) + if _got_release: + d_o_e(d_w) + return _keysdown.keys() + +def keys_waiting(): + global _keyswaiting + keys = _keyswaiting.keys() + _keyswaiting = {} + return keys + +# Block for a list of keys... + +def wait_for_keys(): + keys = [] + while keys == []: + keys = keys_pressed() + sleep(0.05) + return keys + +def remove_from_screen(x, + d_o_e=Tkinter.tkinter.dooneevent, + d_w=Tkinter.tkinter.DONT_WAIT): + _canvas.delete(x) + d_o_e(d_w) + +def _adjust_coords(coord_list, x, y): + for i in range(0, len(coord_list), 2): + coord_list[i] = coord_list[i] + x + coord_list[i + 1] = coord_list[i + 1] + y + return coord_list + +def move_to(object, x, y=None, + d_o_e=Tkinter.tkinter.dooneevent, + d_w=Tkinter.tkinter.DONT_WAIT): + if y is None: + try: x, y = x + except: raise 'incomprehensible coordinates' + + horiz = True + newCoords = [] + current_x, current_y = _canvas.coords(object)[0:2] # first point + for coord in _canvas.coords(object): + if horiz: + inc = x - current_x + else: + inc = y - current_y + horiz = not horiz + + newCoords.append(coord + inc) + + _canvas.coords(object, *newCoords) + d_o_e(d_w) + +def move_by(object, x, y=None, + d_o_e=Tkinter.tkinter.dooneevent, + d_w=Tkinter.tkinter.DONT_WAIT, lift=False): + if y is None: + try: x, y = x + except: raise Exception, 'incomprehensible coordinates' + + horiz = True + newCoords = [] + for coord in _canvas.coords(object): + if horiz: + inc = x + else: + inc = y + horiz = not horiz + + newCoords.append(coord + inc) + + _canvas.coords(object, *newCoords) + d_o_e(d_w) + if lift: + _canvas.tag_raise(object) + +def writePostscript(filename): + "Writes the current canvas to a postscript file." + psfile = file(filename, 'w') + psfile.write(_canvas.postscript(pageanchor='sw', + y='0.c', + x='0.c')) + psfile.close() + +ghost_shape = [ + (0, - 0.5), + (0.25, - 0.75), + (0.5, - 0.5), + (0.75, - 0.75), + (0.75, 0.5), + (0.5, 0.75), + (- 0.5, 0.75), + (- 0.75, 0.5), + (- 0.75, - 0.75), + (- 0.5, - 0.5), + (- 0.25, - 0.75) + ] + +if __name__ == '__main__': + begin_graphics() + clear_screen() + ghost_shape = [(x * 10 + 20, y * 10 + 20) for x, y in ghost_shape] + g = polygon(ghost_shape, formatColor(1, 1, 1)) + move_to(g, (50, 50)) + circle((150, 150), 20, formatColor(0.7, 0.3, 0.0), endpoints=[15, - 15]) + sleep(2) diff --git a/keyboardAgents.py b/keyboardAgents.py new file mode 100644 index 0000000..c7d9fcf --- /dev/null +++ b/keyboardAgents.py @@ -0,0 +1,84 @@ +# keyboardAgents.py +# ----------------- +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). + + +from game import Agent +from game import Directions +import random + +class KeyboardAgent(Agent): + """ + An agent controlled by the keyboard. + """ + # NOTE: Arrow keys also work. + WEST_KEY = 'a' + EAST_KEY = 'd' + NORTH_KEY = 'w' + SOUTH_KEY = 's' + STOP_KEY = 'q' + + def __init__( self, index = 0 ): + + self.lastMove = Directions.STOP + self.index = index + self.keys = [] + + def getAction( self, state): + from graphicsUtils import keys_waiting + from graphicsUtils import keys_pressed + keys = keys_waiting() + keys_pressed() + if keys != []: + self.keys = keys + + legal = state.getLegalActions(self.index) + move = self.getMove(legal) + + if move == Directions.STOP: + # Try to move in the same direction as before + if self.lastMove in legal: + move = self.lastMove + + if (self.STOP_KEY in self.keys) and Directions.STOP in legal: move = Directions.STOP + + if move not in legal: + move = random.choice(legal) + + self.lastMove = move + return move + + def getMove(self, legal): + move = Directions.STOP + if (self.WEST_KEY in self.keys or 'Left' in self.keys) and Directions.WEST in legal: move = Directions.WEST + if (self.EAST_KEY in self.keys or 'Right' in self.keys) and Directions.EAST in legal: move = Directions.EAST + if (self.NORTH_KEY in self.keys or 'Up' in self.keys) and Directions.NORTH in legal: move = Directions.NORTH + if (self.SOUTH_KEY in self.keys or 'Down' in self.keys) and Directions.SOUTH in legal: move = Directions.SOUTH + return move + +class KeyboardAgent2(KeyboardAgent): + """ + A second agent controlled by the keyboard. + """ + # NOTE: Arrow keys also work. + WEST_KEY = 'j' + EAST_KEY = "l" + NORTH_KEY = 'i' + SOUTH_KEY = 'k' + STOP_KEY = 'u' + + def getMove(self, legal): + move = Directions.STOP + if (self.WEST_KEY in self.keys) and Directions.WEST in legal: move = Directions.WEST + if (self.EAST_KEY in self.keys) and Directions.EAST in legal: move = Directions.EAST + if (self.NORTH_KEY in self.keys) and Directions.NORTH in legal: move = Directions.NORTH + if (self.SOUTH_KEY in self.keys) and Directions.SOUTH in legal: move = Directions.SOUTH + return move diff --git a/layout.py b/layout.py new file mode 100644 index 0000000..c6b377d --- /dev/null +++ b/layout.py @@ -0,0 +1,149 @@ +# layout.py +# --------- +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). + + +from util import manhattanDistance +from game import Grid +import os +import random + +VISIBILITY_MATRIX_CACHE = {} + +class Layout: + """ + A Layout manages the static information about the game board. + """ + + def __init__(self, layoutText): + self.width = len(layoutText[0]) + self.height= len(layoutText) + self.walls = Grid(self.width, self.height, False) + self.food = Grid(self.width, self.height, False) + self.capsules = [] + self.agentPositions = [] + self.numGhosts = 0 + self.processLayoutText(layoutText) + self.layoutText = layoutText + self.totalFood = len(self.food.asList()) + # self.initializeVisibilityMatrix() + + def getNumGhosts(self): + return self.numGhosts + + def initializeVisibilityMatrix(self): + global VISIBILITY_MATRIX_CACHE + if reduce(str.__add__, self.layoutText) not in VISIBILITY_MATRIX_CACHE: + from game import Directions + vecs = [(-0.5,0), (0.5,0),(0,-0.5),(0,0.5)] + dirs = [Directions.NORTH, Directions.SOUTH, Directions.WEST, Directions.EAST] + vis = Grid(self.width, self.height, {Directions.NORTH:set(), Directions.SOUTH:set(), Directions.EAST:set(), Directions.WEST:set(), Directions.STOP:set()}) + for x in range(self.width): + for y in range(self.height): + if self.walls[x][y] == False: + for vec, direction in zip(vecs, dirs): + dx, dy = vec + nextx, nexty = x + dx, y + dy + while (nextx + nexty) != int(nextx) + int(nexty) or not self.walls[int(nextx)][int(nexty)] : + vis[x][y][direction].add((nextx, nexty)) + nextx, nexty = x + dx, y + dy + self.visibility = vis + VISIBILITY_MATRIX_CACHE[reduce(str.__add__, self.layoutText)] = vis + else: + self.visibility = VISIBILITY_MATRIX_CACHE[reduce(str.__add__, self.layoutText)] + + def isWall(self, pos): + x, col = pos + return self.walls[x][col] + + def getRandomLegalPosition(self): + x = random.choice(range(self.width)) + y = random.choice(range(self.height)) + while self.isWall( (x, y) ): + x = random.choice(range(self.width)) + y = random.choice(range(self.height)) + return (x,y) + + def getRandomCorner(self): + poses = [(1,1), (1, self.height - 2), (self.width - 2, 1), (self.width - 2, self.height - 2)] + return random.choice(poses) + + def getFurthestCorner(self, pacPos): + poses = [(1,1), (1, self.height - 2), (self.width - 2, 1), (self.width - 2, self.height - 2)] + dist, pos = max([(manhattanDistance(p, pacPos), p) for p in poses]) + return pos + + def isVisibleFrom(self, ghostPos, pacPos, pacDirection): + row, col = [int(x) for x in pacPos] + return ghostPos in self.visibility[row][col][pacDirection] + + def __str__(self): + return "\n".join(self.layoutText) + + def deepCopy(self): + return Layout(self.layoutText[:]) + + def processLayoutText(self, layoutText): + """ + Coordinates are flipped from the input format to the (x,y) convention here + + The shape of the maze. Each character + represents a different type of object. + % - Wall + . - Food + o - Capsule + G - Ghost + P - Pacman + Other characters are ignored. + """ + maxY = self.height - 1 + for y in range(self.height): + for x in range(self.width): + layoutChar = layoutText[maxY - y][x] + self.processLayoutChar(x, y, layoutChar) + self.agentPositions.sort() + self.agentPositions = [ ( i == 0, pos) for i, pos in self.agentPositions] + + def processLayoutChar(self, x, y, layoutChar): + if layoutChar == '%': + self.walls[x][y] = True + elif layoutChar == '.': + self.food[x][y] = True + elif layoutChar == 'o': + self.capsules.append((x, y)) + elif layoutChar == 'P': + self.agentPositions.append( (0, (x, y) ) ) + elif layoutChar in ['G']: + self.agentPositions.append( (1, (x, y) ) ) + self.numGhosts += 1 + elif layoutChar in ['1', '2', '3', '4']: + self.agentPositions.append( (int(layoutChar), (x,y))) + self.numGhosts += 1 +def getLayout(name, back = 2): + if name.endswith('.lay'): + layout = tryToLoad('layouts/' + name) + if layout == None: layout = tryToLoad(name) + else: + layout = tryToLoad('layouts/' + name + '.lay') + if layout == None: layout = tryToLoad(name + '.lay') + if layout == None and back >= 0: + curdir = os.path.abspath('.') + os.chdir('..') + layout = getLayout(name, back -1) + os.chdir(curdir) + return layout + +def tryToLoad(fullname): + if(not os.path.exists(fullname)): return None + f = open(fullname) + try: return Layout([line.strip() for line in f]) + finally: f.close() diff --git a/layouts/bigCorners.lay b/layouts/bigCorners.lay new file mode 100644 index 0000000..4d89d7b --- /dev/null +++ b/layouts/bigCorners.lay @@ -0,0 +1,37 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%. % %.% +% %%%%% % %%% %%% %%%%%%% % % +% % % % % % % % +%%%%% %%%%% %%% % % % %%% %%%%% % %%% +% % % % % % % % % % % % % +% %%% % % % %%% %%%%% %%% % %%% %%% % +% % % % % % % % % +%%% %%%%%%%%% %%%%%%% %%% %%% % % % % +% % % % % % % +% % %%%%% % %%% % % %%% % %%% %%% % % +% % % % % % % % % % % % % % +% % % %%%%%%% % %%%%%%%%% %%% % %%% % +% % % % % % % % % % +%%% %%% % %%%%% %%%%% %%% %%% %%%%% % +% % % % % % % % % +% % % % % % %%% %%% %%% % % % % % % +% % % % % %% % % % % % % % % % +% % %%%%% % %%% %%% % %%% %%% %%%%% +% % % % % % % % % % % +% %%% % % % %%% %%% %%%%%%%%% % %%% +% % % % % % % +% %%% %%%%%%%%%%%%%%%%%%%%% % % %%% % +% % % % +% % % %%%%% %%% % % % % %%%%%%%%%%%%% +% % % % % % % % % % % % +% % %%% %%% % % % %%%%%%%%% %%% % % % +% % % % % % %P % % % % % % +% %%% %%% %%% % %%% % % %%%%% % %%%%% +% % % % % % % % +%%% % %%%%% %%%%% %%% %%% % %%% % %%% +% % % % % % % % % % % % % % % +% % %%% % % % % %%%%%%%%% % % % % % % +% % % % +% % % %%% %%% %%%%%%% %%% %%% %%% % +%.% % % % % .% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \ No newline at end of file diff --git a/layouts/bigMaze.lay b/layouts/bigMaze.lay new file mode 100644 index 0000000..e11fade --- /dev/null +++ b/layouts/bigMaze.lay @@ -0,0 +1,37 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % % % % % % % +% %%%%%%% % %%% % %%% %%% %%%%%%% % % +% % % % % % % % +%%%%% %%%%% %%% % % % %%% %%%%% % %%% +% % % % % % % % % % % % % % +% %%% % % % %%% %%%%% %%% % %%% %%% % +% % % % % % % % % +%%% %%%%%%%%% %%%%%%% %%% %%% % % % % +% % % % % % % +% % %%%%% % %%% % % %%% % %%% %%% % % +% % % % % % % % % % % % % % +% % % %%%%%%% % %%%%%%%%% %%% % %%% % +% % % % % % % % % % +%%% %%% % %%%%% %%%%% %%% %%% %%%%% % +% % % % % % % % % % % % +% % % % % %%% %%% %%% %%% % % % % % % +% % % % % % % % % +%%% %%%%%%% % % %%%%% %%% % %%% %%%%% +% % % % % % % % % % +%%%%% % % %%%%%%%%% %%%%%%%%%%% % %%% +% % % % % % % % % +% %%% %%%%% %%%%%%%%% %%%%% % % %%% % +% % % % % % % +% % % %%%%% %%% % % % % %%%%%%%%%%%%% +% % % % % % % % % % % % +% % %%% %%% % % % %%%%%%%%% %%% % % % +% % % % % % % % % % % % % +% %%% %%% %%%%% %%% % % %%%%% % %%%%% +% % % % % % % % % +%%% % %%%%% %%%%% %%% %%% % %%% % %%% +% % % % % % % % % % % % % % % +% % %%% % % % % %%%%%%%%% % % % % % % +% % % % % % +% % % % %%% %%% %%%%%%% %%% %%% %%% % +%.% % % % % % % % P% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \ No newline at end of file diff --git a/layouts/bigSafeSearch.lay b/layouts/bigSafeSearch.lay new file mode 100644 index 0000000..b5fd414 --- /dev/null +++ b/layouts/bigSafeSearch.lay @@ -0,0 +1,8 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%.%.........%% G % o%%%%.....% +%.%.%%%%%%%.%%%%%% %%%%%%%.%%.% +%............%...%............% +%%%%%...%%%.. ..%.%...%.%%% +%o%%%.%%%%%.%%%%%%%.%%%.%.%%%%% +% ..........Po...%...%. o% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/layouts/bigSearch.lay b/layouts/bigSearch.lay new file mode 100644 index 0000000..bb59eb8 --- /dev/null +++ b/layouts/bigSearch.lay @@ -0,0 +1,15 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%.....%.................%.....% +%.%%%.%.%%%.%%%%%%%.%%%.%.....% +%.%...%.%......%......%.%.....% +%...%%%.%.%%%%.%.%%%%...%%%...% +%%%.%.%.%.%......%..%.%...%.%%% +%...%.%%%.%.%%% %%%.%.%%%.%...% +%.%%%.......% %.......%%%.% +%...%.%%%%%.%%%%%%%.%.%%%.%...% +%%%.%...%.%....%....%.%...%.%%% +%...%%%.%.%%%%.%.%%%%.%.%%%...% +%.......%......%......%.....%.% +%.....%.%%%.%%%%%%%.%%%.%.%%%.% +%.....%........P....%...%.....% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/layouts/boxSearch.lay b/layouts/boxSearch.lay new file mode 100644 index 0000000..4a113fc --- /dev/null +++ b/layouts/boxSearch.lay @@ -0,0 +1,14 @@ +%%%%%%%%%%%%%% +%. . . . . % % +% % % +%. . . . . %G% +% % % +%. . . . . % % +% % % +%. . . . . % % +% P %G% +%. . . . . % % +% % % +%. . . . . % % +% % % +%%%%%%%%%%%%%% diff --git a/layouts/capsuleClassic.lay b/layouts/capsuleClassic.lay new file mode 100644 index 0000000..06a5c51 --- /dev/null +++ b/layouts/capsuleClassic.lay @@ -0,0 +1,7 @@ +%%%%%%%%%%%%%%%%%%% +%G. G ....% +%.% % %%%%%% %.%%.% +%.%o% % o% %.o%.% +%.%%%.% %%% %..%.% +%..... P %..%G% +%%%%%%%%%%%%%%%%%%%% diff --git a/layouts/contestClassic.lay b/layouts/contestClassic.lay new file mode 100644 index 0000000..84c8733 --- /dev/null +++ b/layouts/contestClassic.lay @@ -0,0 +1,9 @@ +%%%%%%%%%%%%%%%%%%%% +%o...%........%...o% +%.%%.%.%%..%%.%.%%.% +%...... G GG%......% +%.%.%%.%% %%%.%%.%.% +%.%....% ooo%.%..%.% +%.%.%%.% %% %.%.%%.% +%o%......P....%....% +%%%%%%%%%%%%%%%%%%%% diff --git a/layouts/contoursMaze.lay b/layouts/contoursMaze.lay new file mode 100644 index 0000000..a068956 --- /dev/null +++ b/layouts/contoursMaze.lay @@ -0,0 +1,11 @@ +%%%%%%%%%%%%%%%%%%%%% +% % +% % +% % +% % +% P % +% % +% % +% % +%. % +%%%%%%%%%%%%%%%%%%%%% \ No newline at end of file diff --git a/layouts/greedySearch.lay b/layouts/greedySearch.lay new file mode 100644 index 0000000..4072363 --- /dev/null +++ b/layouts/greedySearch.lay @@ -0,0 +1,8 @@ +%%%%%% +%....% +% %%.% +% %%.% +%.P .% +%.%%%% +%....% +%%%%%% \ No newline at end of file diff --git a/layouts/mediumClassic.lay b/layouts/mediumClassic.lay new file mode 100644 index 0000000..33c5db8 --- /dev/null +++ b/layouts/mediumClassic.lay @@ -0,0 +1,11 @@ +%%%%%%%%%%%%%%%%%%%% +%o...%........%....% +%.%%.%.%%%%%%.%.%%.% +%.%..............%.% +%.%.%%.%% %%.%%.%.% +%......%G G%......% +%.%.%%.%%%%%%.%%.%.% +%.%..............%.% +%.%%.%.%%%%%%.%.%%.% +%....%...P....%...o% +%%%%%%%%%%%%%%%%%%%% diff --git a/layouts/mediumCorners.lay b/layouts/mediumCorners.lay new file mode 100644 index 0000000..6a39756 --- /dev/null +++ b/layouts/mediumCorners.lay @@ -0,0 +1,14 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%. % % % %.% +% % % %%%%%% %%%%%%% % % +% % % % % % +%%%%% %%%%% %%% %% %%%%% % %%% +% % % % % % % % % +% %%% % % % %%%%%%%% %%% %%% % +% % %% % % % % +%%% % %%%%%%% %%%% %%% % % % % +% % %% % % % +% % %%%%% % %%%% % %%% %%% % % +% % % % % % %%% % +%. %P%%%%% % %%% % .% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \ No newline at end of file diff --git a/layouts/mediumDottedMaze.lay b/layouts/mediumDottedMaze.lay new file mode 100644 index 0000000..103f818 --- /dev/null +++ b/layouts/mediumDottedMaze.lay @@ -0,0 +1,18 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% P% +% %%%%%%%%%%%%%%%%%%% %%% %%%%%%%% % +% %% % % %%% %%% %% ... % +% %% % % % % %%%% %%%%%%%%% %% %%%%% +% %% % % % % % %% %% %% ... % +% %% % % % % % %%%% %%% %%%%%% % +% % % % % % %% %%%%%%%% ... % +% %% % % %%%%%%%% %% %% %%%%% +% %% % %% %%%%%%%%% %% ... % +% %%%%%% %%%%%%% %% %%%%%% % +%%%%%% % %%%% %% % ... % +% %%%%%% %%%%% % %% %% %%%%% +% %%%%%% % %%%%% %% % +% %%%%%% %%%%%%%%%%% %% %% % +%%%%%%%%%% %%%%%% % +%. %%%%%%%%%%%%%%%% ...... % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \ No newline at end of file diff --git a/layouts/mediumMaze.lay b/layouts/mediumMaze.lay new file mode 100644 index 0000000..55c1236 --- /dev/null +++ b/layouts/mediumMaze.lay @@ -0,0 +1,18 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% P% +% %%%%%%%%%%%%%%%%%%%%%%% %%%%%%%% % +% %% % % %%%%%%% %% % +% %% % % % % %%%% %%%%%%%%% %% %%%%% +% %% % % % % %% %% % +% %% % % % % % %%%% %%% %%%%%% % +% % % % % % %% %%%%%%%% % +% %% % % %%%%%%%% %% %% %%%%% +% %% % %% %%%%%%%%% %% % +% %%%%%% %%%%%%% %% %%%%%% % +%%%%%% % %%%% %% % % +% %%%%%% %%%%% % %% %% %%%%% +% %%%%%% % %%%%% %% % +% %%%%%% %%%%%%%%%%% %% %% % +%%%%%%%%%% %%%%%% % +%. %%%%%%%%%%%%%%%% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \ No newline at end of file diff --git a/layouts/mediumSafeSearch.lay b/layouts/mediumSafeSearch.lay new file mode 100644 index 0000000..e7d6b1c --- /dev/null +++ b/layouts/mediumSafeSearch.lay @@ -0,0 +1,6 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%.% ....%% G %%%%%% o%%.% +%.%o%%%%%%%.%%%%%%% %%%%%.% +% %%%.%%%%%.%%%%%%%.%%%.%.%%%.% +% ..........Po...%.........% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/layouts/mediumScaryMaze.lay b/layouts/mediumScaryMaze.lay new file mode 100644 index 0000000..65d4c33 --- /dev/null +++ b/layouts/mediumScaryMaze.lay @@ -0,0 +1,18 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% P% +% %%%%%%%%%%%%%%%%%%% %%% %%%%%%%% % +% %% % % %%% %%% %%GG % +% %% % % % % %%%% %%%%%%%%% %% %%%%% +% %% % % % % % %%GG %% % +% %% % % % % % %%%%% %%% %%%%%% % +% %% % % % % %% %%%%%%%%% % +% %% % % %%%%%%%% %% %% %%%%% +% %% % %% %%%%%%%%% %% % +% %%% %% %%%%%%% %% %%%%%% % +%%%%%% % % %% %% % +% %%%%%% %% %% %% %% %%%%% +% %%%%%% % %%%%% %% % +% %%%% %%%%% %%%%%% % +%%%%%%%% % %%%%%% % +%. %%%%%%%%%%%%%%%% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \ No newline at end of file diff --git a/layouts/mediumSearch.lay b/layouts/mediumSearch.lay new file mode 100644 index 0000000..2f8af42 --- /dev/null +++ b/layouts/mediumSearch.lay @@ -0,0 +1,8 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%............%%%%%............% +%%%.%...%%%.........%.%...%.%%% +%...%%%.%.%%%%.%.%%%%%%.%%%...% +%.%.....%......%......%.....%.% +%.%%%.%%%%%.%%%%%%%.%%%.%.%%%%% +%.....%........P....%...%.....% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/layouts/minimaxClassic.lay b/layouts/minimaxClassic.lay new file mode 100644 index 0000000..a547397 --- /dev/null +++ b/layouts/minimaxClassic.lay @@ -0,0 +1,5 @@ +%%%%%%%%% +%.P G% +% %.%G%%% +%G %%% +%%%%%%%%% diff --git a/layouts/oddSearch.lay b/layouts/oddSearch.lay new file mode 100644 index 0000000..2ddbc9a --- /dev/null +++ b/layouts/oddSearch.lay @@ -0,0 +1,7 @@ +%%%%%%%%%%%%%%%%%%%% +%...%.........%%...% +%.%.%.%%%%%%%%%%.%.% +%..................% +%%%%%%%%.%.%%%%%%%P% +%%%%%%%%....... % +%%%%%%%%%%%%%%%%%%%% diff --git a/layouts/openClassic.lay b/layouts/openClassic.lay new file mode 100644 index 0000000..6760b42 --- /dev/null +++ b/layouts/openClassic.lay @@ -0,0 +1,9 @@ +%%%%%%%%%%%%%%%%%%%%%%%%% +%.. P .... .... % +%.. ... ... ... ... % +%.. ... ... ... ... % +%.. .... .... G % +%.. ... ... ... ... % +%.. ... ... ... ... % +%.. .... .... o% +%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/layouts/openMaze.lay b/layouts/openMaze.lay new file mode 100644 index 0000000..5dee689 --- /dev/null +++ b/layouts/openMaze.lay @@ -0,0 +1,23 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% P% +% % % +% % % +% % % +% % % +% % % +% % % % +% % % % +% % % % +% % % % +% % % % +% % % % +% % % % +%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%% +% % % +% % % +% % % +% % +% % +% % +%. % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \ No newline at end of file diff --git a/layouts/openSearch.lay b/layouts/openSearch.lay new file mode 100644 index 0000000..f02d21d --- /dev/null +++ b/layouts/openSearch.lay @@ -0,0 +1,7 @@ +%%%%%%%%%%%%%%%%%%%% +%..................% +%..................% +%........P.........% +%..................% +%..................% +%%%%%%%%%%%%%%%%%%%% diff --git a/layouts/originalClassic.lay b/layouts/originalClassic.lay new file mode 100644 index 0000000..b2770c5 --- /dev/null +++ b/layouts/originalClassic.lay @@ -0,0 +1,27 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%............%%............% +%.%%%%.%%%%%.%%.%%%%%.%%%%.% +%o%%%%.%%%%%.%%.%%%%%.%%%%o% +%.%%%%.%%%%%.%%.%%%%%.%%%%.% +%..........................% +%.%%%%.%%.%%%%%%%%.%%.%%%%.% +%.%%%%.%%.%%%%%%%%.%%.%%%%.% +%......%%....%%....%%......% +%%%%%%.%%%%% %% %%%%%.%%%%%% +%%%%%%.%%%%% %% %%%%%.%%%%%% +%%%%%%.% %.%%%%%% +%%%%%%.% %%%% %%%% %.%%%%%% +% . %G GG G% . % +%%%%%%.% %%%%%%%%%% %.%%%%%% +%%%%%%.% %.%%%%%% +%%%%%%.% %%%%%%%%%% %.%%%%%% +%............%%............% +%.%%%%.%%%%%.%%.%%%%%.%%%%.% +%.%%%%.%%%%%.%%.%%%%%.%%%%.% +%o..%%....... .......%%..o% +%%%.%%.%%.%%%%%%%%.%%.%%.%%% +%%%.%%.%%.%%%%%%%%.%%.%%.%%% +%......%%....%%....%%......% +%.%%%%%%%%%%.%%.%%%%%%%%%%.% +%.............P............% +%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/layouts/powerClassic.lay b/layouts/powerClassic.lay new file mode 100644 index 0000000..3f3d983 --- /dev/null +++ b/layouts/powerClassic.lay @@ -0,0 +1,7 @@ +%%%%%%%%%%%%%%%%%%%% +%o....o%GGGG%o....o% +%..%...%% %%...%..% +%.%o.%........%.o%.% +%.o%.%.%%%%%%.%.%o.% +%........P.........% +%%%%%%%%%%%%%%%%%%%% diff --git a/layouts/smallClassic.lay b/layouts/smallClassic.lay new file mode 100644 index 0000000..ce6c1d9 --- /dev/null +++ b/layouts/smallClassic.lay @@ -0,0 +1,7 @@ +%%%%%%%%%%%%%%%%%%%% +%......%G G%......% +%.%%...%% %%...%%.% +%.%o.%........%.o%.% +%.%%.%.%%%%%%.%.%%.% +%........P.........% +%%%%%%%%%%%%%%%%%%%% diff --git a/layouts/smallMaze.lay b/layouts/smallMaze.lay new file mode 100644 index 0000000..72d3ffc --- /dev/null +++ b/layouts/smallMaze.lay @@ -0,0 +1,10 @@ +%%%%%%%%%%%%%%%%%%%%%% +% %% % % % +% %%%%%% % %%%%%% % +%%%%%% P % % +% % %%%%%% %% %%%%% +% %%%% % % % +% %%% %%% % % +%%%%%%%%%% %%%%%% % +%. %% % +%%%%%%%%%%%%%%%%%%%%%% \ No newline at end of file diff --git a/layouts/smallSafeSearch.lay b/layouts/smallSafeSearch.lay new file mode 100644 index 0000000..b97feaa --- /dev/null +++ b/layouts/smallSafeSearch.lay @@ -0,0 +1,15 @@ +%%%%%%%%% +%.. % G % +%%% %%%%% +% % +%%%%%%% % +% % +% %%%%% % +% % % +%%%%% % % +% %o% +% %%%%%%% +% .% +%%%%%%%.% +%Po .% +%%%%%%%%% diff --git a/layouts/smallSearch.lay b/layouts/smallSearch.lay new file mode 100644 index 0000000..c2321d4 --- /dev/null +++ b/layouts/smallSearch.lay @@ -0,0 +1,5 @@ +%%%%%%%%%%%%%%%%%%%% +%. ...P .% +%.%%.%%.%%.%%.%% %.% +% %% %..... %.% +%%%%%%%%%%%%%%%%%%%% diff --git a/layouts/testClassic.lay b/layouts/testClassic.lay new file mode 100644 index 0000000..4b3ffca --- /dev/null +++ b/layouts/testClassic.lay @@ -0,0 +1,10 @@ +%%%%% +% . % +%.G.% +% . % +%. .% +% % +% .% +% % +%P .% +%%%%% diff --git a/layouts/testMaze.lay b/layouts/testMaze.lay new file mode 100644 index 0000000..4d259a4 --- /dev/null +++ b/layouts/testMaze.lay @@ -0,0 +1,3 @@ +%%%%%%%%%% +%. P% +%%%%%%%%%% diff --git a/layouts/testSearch.lay b/layouts/testSearch.lay new file mode 100644 index 0000000..25bad23 --- /dev/null +++ b/layouts/testSearch.lay @@ -0,0 +1,5 @@ +%%%%% +%.P % +%%% % +%. % +%%%%% diff --git a/layouts/tinyCorners.lay b/layouts/tinyCorners.lay new file mode 100644 index 0000000..526c880 --- /dev/null +++ b/layouts/tinyCorners.lay @@ -0,0 +1,8 @@ +%%%%%%%% +%. .% +% P % +% %%%% % +% % % +% % %%%% +%.% .% +%%%%%%%% diff --git a/layouts/tinyMaze.lay b/layouts/tinyMaze.lay new file mode 100644 index 0000000..f7035a5 --- /dev/null +++ b/layouts/tinyMaze.lay @@ -0,0 +1,7 @@ +%%%%%%% +% P% +% %%% % +% % % +%% %% +%. %%%% +%%%%%%% diff --git a/layouts/tinySafeSearch.lay b/layouts/tinySafeSearch.lay new file mode 100644 index 0000000..fea6860 --- /dev/null +++ b/layouts/tinySafeSearch.lay @@ -0,0 +1,7 @@ +%%%%%%%%% +% G %...% +%%%%%%% % +%Po % +%.%%.%%.% +%.%%....% +%%%%%%%%% diff --git a/layouts/tinySearch.lay b/layouts/tinySearch.lay new file mode 100644 index 0000000..c51f4b0 --- /dev/null +++ b/layouts/tinySearch.lay @@ -0,0 +1,7 @@ +%%%%%%%%% +%.. ..% +%%%%.%% % +% P % +%.%% %%.% +%.%. .% +%%%%%%%%% diff --git a/layouts/trappedClassic.lay b/layouts/trappedClassic.lay new file mode 100644 index 0000000..289557f --- /dev/null +++ b/layouts/trappedClassic.lay @@ -0,0 +1,5 @@ +%%%%%%%% +% P G% +%G%%%%%% +%.... % +%%%%%%%% diff --git a/layouts/trickyClassic.lay b/layouts/trickyClassic.lay new file mode 100644 index 0000000..ffa156c --- /dev/null +++ b/layouts/trickyClassic.lay @@ -0,0 +1,13 @@ +%%%%%%%%%%%%%%%%%%%% +%o...%........%...o% +%.%%.%.%%..%%.%.%%.% +%.%.....%..%.....%.% +%.%.%%.%% %%.%%.%.% +%...... GGGG%.%....% +%.%....%%%%%%.%..%.% +%.%....% oo%.%..%.% +%.%....% %%%%.%..%.% +%.%...........%..%.% +%.%%.%.%%%%%%.%.%%.% +%o...%...P....%...o% +%%%%%%%%%%%%%%%%%%%% diff --git a/layouts/trickySearch.lay b/layouts/trickySearch.lay new file mode 100644 index 0000000..4a607e6 --- /dev/null +++ b/layouts/trickySearch.lay @@ -0,0 +1,7 @@ +%%%%%%%%%%%%%%%%%%%% +%. ..% % +%.%%.%%.%%.%%.%% % % +% P % % +%%%%%%%%%%%%%%%%%% % +%..... % +%%%%%%%%%%%%%%%%%%%% diff --git a/pacman.py b/pacman.py new file mode 100644 index 0000000..740451d --- /dev/null +++ b/pacman.py @@ -0,0 +1,684 @@ +# pacman.py +# --------- +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). + + +""" +Pacman.py holds the logic for the classic pacman game along with the main +code to run a game. This file is divided into three sections: + + (i) Your interface to the pacman world: + Pacman is a complex environment. You probably don't want to + read through all of the code we wrote to make the game runs + correctly. This section contains the parts of the code + that you will need to understand in order to complete the + project. There is also some code in game.py that you should + understand. + + (ii) The hidden secrets of pacman: + This section contains all of the logic code that the pacman + environment uses to decide who can move where, who dies when + things collide, etc. You shouldn't need to read this section + of code, but you can if you want. + + (iii) Framework to start a game: + The final section contains the code for reading the command + you use to set up the game, then starting up a new game, along with + linking in all the external parts (agent functions, graphics). + Check this section out to see all the options available to you. + +To play your first game, type 'python pacman.py' from the command line. +The keys are 'a', 's', 'd', and 'w' to move (or arrow keys). Have fun! +""" +from game import GameStateData +from game import Game +from game import Directions +from game import Actions +from util import nearestPoint +from util import manhattanDistance +import util, layout +import sys, types, time, random, os + +################################################### +# YOUR INTERFACE TO THE PACMAN WORLD: A GameState # +################################################### + +class GameState: + """ + A GameState specifies the full game state, including the food, capsules, + agent configurations and score changes. + + GameStates are used by the Game object to capture the actual state of the game and + can be used by agents to reason about the game. + + Much of the information in a GameState is stored in a GameStateData object. We + strongly suggest that you access that data via the accessor methods below rather + than referring to the GameStateData object directly. + + Note that in classic Pacman, Pacman is always agent 0. + """ + + #################################################### + # Accessor methods: use these to access state data # + #################################################### + + # static variable keeps track of which states have had getLegalActions called + explored = set() + def getAndResetExplored(): + tmp = GameState.explored.copy() + GameState.explored = set() + return tmp + getAndResetExplored = staticmethod(getAndResetExplored) + + def getLegalActions( self, agentIndex=0 ): + """ + Returns the legal actions for the agent specified. + """ +# GameState.explored.add(self) + if self.isWin() or self.isLose(): return [] + + if agentIndex == 0: # Pacman is moving + return PacmanRules.getLegalActions( self ) + else: + return GhostRules.getLegalActions( self, agentIndex ) + + def generateSuccessor( self, agentIndex, action): + """ + Returns the successor state after the specified agent takes the action. + """ + # Check that successors exist + if self.isWin() or self.isLose(): raise Exception('Can\'t generate a successor of a terminal state.') + + # Copy current state + state = GameState(self) + + # Let agent's logic deal with its action's effects on the board + if agentIndex == 0: # Pacman is moving + state.data._eaten = [False for i in range(state.getNumAgents())] + PacmanRules.applyAction( state, action ) + else: # A ghost is moving + GhostRules.applyAction( state, action, agentIndex ) + + # Time passes + if agentIndex == 0: + state.data.scoreChange += -TIME_PENALTY # Penalty for waiting around + else: + GhostRules.decrementTimer( state.data.agentStates[agentIndex] ) + + # Resolve multi-agent effects + GhostRules.checkDeath( state, agentIndex ) + + # Book keeping + state.data._agentMoved = agentIndex + state.data.score += state.data.scoreChange + GameState.explored.add(self) + GameState.explored.add(state) + return state + + def getLegalPacmanActions( self ): + return self.getLegalActions( 0 ) + + def generatePacmanSuccessor( self, action ): + """ + Generates the successor state after the specified pacman move + """ + return self.generateSuccessor( 0, action ) + + def getPacmanState( self ): + """ + Returns an AgentState object for pacman (in game.py) + + state.pos gives the current position + state.direction gives the travel vector + """ + return self.data.agentStates[0].copy() + + def getPacmanPosition( self ): + return self.data.agentStates[0].getPosition() + + def getGhostStates( self ): + return self.data.agentStates[1:] + + def getGhostState( self, agentIndex ): + if agentIndex == 0 or agentIndex >= self.getNumAgents(): + raise Exception("Invalid index passed to getGhostState") + return self.data.agentStates[agentIndex] + + def getGhostPosition( self, agentIndex ): + if agentIndex == 0: + raise Exception("Pacman's index passed to getGhostPosition") + return self.data.agentStates[agentIndex].getPosition() + + def getGhostPositions(self): + return [s.getPosition() for s in self.getGhostStates()] + + def getNumAgents( self ): + return len( self.data.agentStates ) + + def getScore( self ): + return float(self.data.score) + + def getCapsules(self): + """ + Returns a list of positions (x,y) of the remaining capsules. + """ + return self.data.capsules + + def getNumFood( self ): + return self.data.food.count() + + def getFood(self): + """ + Returns a Grid of boolean food indicator variables. + + Grids can be accessed via list notation, so to check + if there is food at (x,y), just call + + currentFood = state.getFood() + if currentFood[x][y] == True: ... + """ + return self.data.food + + def getWalls(self): + """ + Returns a Grid of boolean wall indicator variables. + + Grids can be accessed via list notation, so to check + if there is a wall at (x,y), just call + + walls = state.getWalls() + if walls[x][y] == True: ... + """ + return self.data.layout.walls + + def hasFood(self, x, y): + return self.data.food[x][y] + + def hasWall(self, x, y): + return self.data.layout.walls[x][y] + + def isLose( self ): + return self.data._lose + + def isWin( self ): + return self.data._win + + ############################################# + # Helper methods: # + # You shouldn't need to call these directly # + ############################################# + + def __init__( self, prevState = None ): + """ + Generates a new state by copying information from its predecessor. + """ + if prevState != None: # Initial state + self.data = GameStateData(prevState.data) + else: + self.data = GameStateData() + + def deepCopy( self ): + state = GameState( self ) + state.data = self.data.deepCopy() + return state + + def __eq__( self, other ): + """ + Allows two states to be compared. + """ + return hasattr(other, 'data') and self.data == other.data + + def __hash__( self ): + """ + Allows states to be keys of dictionaries. + """ + return hash( self.data ) + + def __str__( self ): + + return str(self.data) + + def initialize( self, layout, numGhostAgents=1000 ): + """ + Creates an initial game state from a layout array (see layout.py). + """ + self.data.initialize(layout, numGhostAgents) + +############################################################################ +# THE HIDDEN SECRETS OF PACMAN # +# # +# You shouldn't need to look through the code in this section of the file. # +############################################################################ + +SCARED_TIME = 40 # Moves ghosts are scared +COLLISION_TOLERANCE = 0.7 # How close ghosts must be to Pacman to kill +TIME_PENALTY = 1 # Number of points lost each round + +class ClassicGameRules: + """ + These game rules manage the control flow of a game, deciding when + and how the game starts and ends. + """ + def __init__(self, timeout=30): + self.timeout = timeout + + def newGame( self, layout, pacmanAgent, ghostAgents, display, quiet = False, catchExceptions=False): + agents = [pacmanAgent] + ghostAgents[:layout.getNumGhosts()] + initState = GameState() + initState.initialize( layout, len(ghostAgents) ) + game = Game(agents, display, self, catchExceptions=catchExceptions) + game.state = initState + self.initialState = initState.deepCopy() + self.quiet = quiet + return game + + def process(self, state, game): + """ + Checks to see whether it is time to end the game. + """ + if state.isWin(): self.win(state, game) + if state.isLose(): self.lose(state, game) + + def win( self, state, game ): + if not self.quiet: print "Pacman emerges victorious! Score: %d" % state.data.score + game.gameOver = True + + def lose( self, state, game ): + if not self.quiet: print "Pacman died! Score: %d" % state.data.score + game.gameOver = True + + def getProgress(self, game): + return float(game.state.getNumFood()) / self.initialState.getNumFood() + + def agentCrash(self, game, agentIndex): + if agentIndex == 0: + print "Pacman crashed" + else: + print "A ghost crashed" + + def getMaxTotalTime(self, agentIndex): + return self.timeout + + def getMaxStartupTime(self, agentIndex): + return self.timeout + + def getMoveWarningTime(self, agentIndex): + return self.timeout + + def getMoveTimeout(self, agentIndex): + return self.timeout + + def getMaxTimeWarnings(self, agentIndex): + return 0 + +class PacmanRules: + """ + These functions govern how pacman interacts with his environment under + the classic game rules. + """ + PACMAN_SPEED=1 + + def getLegalActions( state ): + """ + Returns a list of possible actions. + """ + return Actions.getPossibleActions( state.getPacmanState().configuration, state.data.layout.walls ) + getLegalActions = staticmethod( getLegalActions ) + + def applyAction( state, action ): + """ + Edits the state to reflect the results of the action. + """ + legal = PacmanRules.getLegalActions( state ) + if action not in legal: + raise Exception("Illegal action " + str(action)) + + pacmanState = state.data.agentStates[0] + + # Update Configuration + vector = Actions.directionToVector( action, PacmanRules.PACMAN_SPEED ) + pacmanState.configuration = pacmanState.configuration.generateSuccessor( vector ) + + # Eat + next = pacmanState.configuration.getPosition() + nearest = nearestPoint( next ) + if manhattanDistance( nearest, next ) <= 0.5 : + # Remove food + PacmanRules.consume( nearest, state ) + applyAction = staticmethod( applyAction ) + + def consume( position, state ): + x,y = position + # Eat food + if state.data.food[x][y]: + state.data.scoreChange += 10 + state.data.food = state.data.food.copy() + state.data.food[x][y] = False + state.data._foodEaten = position + # TODO: cache numFood? + numFood = state.getNumFood() + if numFood == 0 and not state.data._lose: + state.data.scoreChange += 500 + state.data._win = True + # Eat capsule + if( position in state.getCapsules() ): + state.data.capsules.remove( position ) + state.data._capsuleEaten = position + # Reset all ghosts' scared timers + for index in range( 1, len( state.data.agentStates ) ): + state.data.agentStates[index].scaredTimer = SCARED_TIME + consume = staticmethod( consume ) + +class GhostRules: + """ + These functions dictate how ghosts interact with their environment. + """ + GHOST_SPEED=1.0 + def getLegalActions( state, ghostIndex ): + """ + Ghosts cannot stop, and cannot turn around unless they + reach a dead end, but can turn 90 degrees at intersections. + """ + conf = state.getGhostState( ghostIndex ).configuration + possibleActions = Actions.getPossibleActions( conf, state.data.layout.walls ) + reverse = Actions.reverseDirection( conf.direction ) + if Directions.STOP in possibleActions: + possibleActions.remove( Directions.STOP ) + if reverse in possibleActions and len( possibleActions ) > 1: + possibleActions.remove( reverse ) + return possibleActions + getLegalActions = staticmethod( getLegalActions ) + + def applyAction( state, action, ghostIndex): + + legal = GhostRules.getLegalActions( state, ghostIndex ) + if action not in legal: + raise Exception("Illegal ghost action " + str(action)) + + ghostState = state.data.agentStates[ghostIndex] + speed = GhostRules.GHOST_SPEED + if ghostState.scaredTimer > 0: speed /= 2.0 + vector = Actions.directionToVector( action, speed ) + ghostState.configuration = ghostState.configuration.generateSuccessor( vector ) + applyAction = staticmethod( applyAction ) + + def decrementTimer( ghostState): + timer = ghostState.scaredTimer + if timer == 1: + ghostState.configuration.pos = nearestPoint( ghostState.configuration.pos ) + ghostState.scaredTimer = max( 0, timer - 1 ) + decrementTimer = staticmethod( decrementTimer ) + + def checkDeath( state, agentIndex): + pacmanPosition = state.getPacmanPosition() + if agentIndex == 0: # Pacman just moved; Anyone can kill him + for index in range( 1, len( state.data.agentStates ) ): + ghostState = state.data.agentStates[index] + ghostPosition = ghostState.configuration.getPosition() + if GhostRules.canKill( pacmanPosition, ghostPosition ): + GhostRules.collide( state, ghostState, index ) + else: + ghostState = state.data.agentStates[agentIndex] + ghostPosition = ghostState.configuration.getPosition() + if GhostRules.canKill( pacmanPosition, ghostPosition ): + GhostRules.collide( state, ghostState, agentIndex ) + checkDeath = staticmethod( checkDeath ) + + def collide( state, ghostState, agentIndex): + if ghostState.scaredTimer > 0: + state.data.scoreChange += 200 + GhostRules.placeGhost(state, ghostState) + ghostState.scaredTimer = 0 + # Added for first-person + state.data._eaten[agentIndex] = True + else: + if not state.data._win: + state.data.scoreChange -= 500 + state.data._lose = True + collide = staticmethod( collide ) + + def canKill( pacmanPosition, ghostPosition ): + return manhattanDistance( ghostPosition, pacmanPosition ) <= COLLISION_TOLERANCE + canKill = staticmethod( canKill ) + + def placeGhost(state, ghostState): + ghostState.configuration = ghostState.start + placeGhost = staticmethod( placeGhost ) + +############################# +# FRAMEWORK TO START A GAME # +############################# + +def default(str): + return str + ' [Default: %default]' + +def parseAgentArgs(str): + if str == None: return {} + pieces = str.split(',') + opts = {} + for p in pieces: + if '=' in p: + key, val = p.split('=') + else: + key,val = p, 1 + opts[key] = val + return opts + +def readCommand( argv ): + """ + Processes the command used to run pacman from the command line. + """ + from optparse import OptionParser + usageStr = """ + USAGE: python pacman.py + EXAMPLES: (1) python pacman.py + - starts an interactive game + (2) python pacman.py --layout smallClassic --zoom 2 + OR python pacman.py -l smallClassic -z 2 + - starts an interactive game on a smaller board, zoomed in + """ + parser = OptionParser(usageStr) + + parser.add_option('-n', '--numGames', dest='numGames', type='int', + help=default('the number of GAMES to play'), metavar='GAMES', default=1) + parser.add_option('-l', '--layout', dest='layout', + help=default('the LAYOUT_FILE from which to load the map layout'), + metavar='LAYOUT_FILE', default='mediumClassic') + parser.add_option('-p', '--pacman', dest='pacman', + help=default('the agent TYPE in the pacmanAgents module to use'), + metavar='TYPE', default='KeyboardAgent') + parser.add_option('-t', '--textGraphics', action='store_true', dest='textGraphics', + help='Display output as text only', default=False) + parser.add_option('-q', '--quietTextGraphics', action='store_true', dest='quietGraphics', + help='Generate minimal output and no graphics', default=False) + parser.add_option('-g', '--ghosts', dest='ghost', + help=default('the ghost agent TYPE in the ghostAgents module to use'), + metavar = 'TYPE', default='RandomGhost') + parser.add_option('-k', '--numghosts', type='int', dest='numGhosts', + help=default('The maximum number of ghosts to use'), default=4) + parser.add_option('-z', '--zoom', type='float', dest='zoom', + help=default('Zoom the size of the graphics window'), default=1.0) + parser.add_option('-f', '--fixRandomSeed', action='store_true', dest='fixRandomSeed', + help='Fixes the random seed to always play the same game', default=False) + parser.add_option('-r', '--recordActions', action='store_true', dest='record', + help='Writes game histories to a file (named by the time they were played)', default=False) + parser.add_option('--replay', dest='gameToReplay', + help='A recorded game file (pickle) to replay', default=None) + parser.add_option('-a','--agentArgs',dest='agentArgs', + help='Comma separated values sent to agent. e.g. "opt1=val1,opt2,opt3=val3"') + parser.add_option('-x', '--numTraining', dest='numTraining', type='int', + help=default('How many episodes are training (suppresses output)'), default=0) + parser.add_option('--frameTime', dest='frameTime', type='float', + help=default('Time to delay between frames; <0 means keyboard'), default=0.1) + parser.add_option('-c', '--catchExceptions', action='store_true', dest='catchExceptions', + help='Turns on exception handling and timeouts during games', default=False) + parser.add_option('--timeout', dest='timeout', type='int', + help=default('Maximum length of time an agent can spend computing in a single game'), default=30) + + options, otherjunk = parser.parse_args(argv) + if len(otherjunk) != 0: + raise Exception('Command line input not understood: ' + str(otherjunk)) + args = dict() + + # Fix the random seed + if options.fixRandomSeed: random.seed('cs188') + + # Choose a layout + args['layout'] = layout.getLayout( options.layout ) + if args['layout'] == None: raise Exception("The layout " + options.layout + " cannot be found") + + # Choose a Pacman agent + noKeyboard = options.gameToReplay == None and (options.textGraphics or options.quietGraphics) + pacmanType = loadAgent(options.pacman, noKeyboard) + agentOpts = parseAgentArgs(options.agentArgs) + if options.numTraining > 0: + args['numTraining'] = options.numTraining + if 'numTraining' not in agentOpts: agentOpts['numTraining'] = options.numTraining + pacman = pacmanType(**agentOpts) # Instantiate Pacman with agentArgs + args['pacman'] = pacman + + # Don't display training games + if 'numTrain' in agentOpts: + options.numQuiet = int(agentOpts['numTrain']) + options.numIgnore = int(agentOpts['numTrain']) + + # Choose a ghost agent + ghostType = loadAgent(options.ghost, noKeyboard) + args['ghosts'] = [ghostType( i+1 ) for i in range( options.numGhosts )] + + # Choose a display format + if options.quietGraphics: + import textDisplay + args['display'] = textDisplay.NullGraphics() + elif options.textGraphics: + import textDisplay + textDisplay.SLEEP_TIME = options.frameTime + args['display'] = textDisplay.PacmanGraphics() + else: + import graphicsDisplay + args['display'] = graphicsDisplay.PacmanGraphics(options.zoom, frameTime = options.frameTime) + args['numGames'] = options.numGames + args['record'] = options.record + args['catchExceptions'] = options.catchExceptions + args['timeout'] = options.timeout + + # Special case: recorded games don't use the runGames method or args structure + if options.gameToReplay != None: + print 'Replaying recorded game %s.' % options.gameToReplay + import cPickle + f = open(options.gameToReplay) + try: recorded = cPickle.load(f) + finally: f.close() + recorded['display'] = args['display'] + replayGame(**recorded) + sys.exit(0) + + return args + +def loadAgent(pacman, nographics): + # Looks through all pythonPath Directories for the right module, + pythonPathStr = os.path.expandvars("$PYTHONPATH") + if pythonPathStr.find(';') == -1: + pythonPathDirs = pythonPathStr.split(':') + else: + pythonPathDirs = pythonPathStr.split(';') + pythonPathDirs.append('.') + + for moduleDir in pythonPathDirs: + if not os.path.isdir(moduleDir): continue + moduleNames = [f for f in os.listdir(moduleDir) if f.endswith('gents.py')] + for modulename in moduleNames: + try: + module = __import__(modulename[:-3]) + except ImportError: + continue + if pacman in dir(module): + if nographics and modulename == 'keyboardAgents.py': + raise Exception('Using the keyboard requires graphics (not text display)') + return getattr(module, pacman) + raise Exception('The agent ' + pacman + ' is not specified in any *Agents.py.') + +def replayGame( layout, actions, display ): + import pacmanAgents, ghostAgents + rules = ClassicGameRules() + agents = [pacmanAgents.GreedyAgent()] + [ghostAgents.RandomGhost(i+1) for i in range(layout.getNumGhosts())] + game = rules.newGame( layout, agents[0], agents[1:], display ) + state = game.state + display.initialize(state.data) + + for action in actions: + # Execute the action + state = state.generateSuccessor( *action ) + # Change the display + display.update( state.data ) + # Allow for game specific conditions (winning, losing, etc.) + rules.process(state, game) + + display.finish() + +def runGames( layout, pacman, ghosts, display, numGames, record, numTraining = 0, catchExceptions=False, timeout=30 ): + import __main__ + __main__.__dict__['_display'] = display + + rules = ClassicGameRules(timeout) + games = [] + + for i in range( numGames ): + beQuiet = i < numTraining + if beQuiet: + # Suppress output and graphics + import textDisplay + gameDisplay = textDisplay.NullGraphics() + rules.quiet = True + else: + gameDisplay = display + rules.quiet = False + game = rules.newGame( layout, pacman, ghosts, gameDisplay, beQuiet, catchExceptions) + game.run() + if not beQuiet: games.append(game) + + if record: + import time, cPickle + fname = ('recorded-game-%d' % (i + 1)) + '-'.join([str(t) for t in time.localtime()[1:6]]) + f = file(fname, 'w') + components = {'layout': layout, 'actions': game.moveHistory} + cPickle.dump(components, f) + f.close() + + if (numGames-numTraining) > 0: + scores = [game.state.getScore() for game in games] + wins = [game.state.isWin() for game in games] + winRate = wins.count(True)/ float(len(wins)) + print 'Average Score:', sum(scores) / float(len(scores)) + print 'Scores: ', ', '.join([str(score) for score in scores]) + print 'Win Rate: %d/%d (%.2f)' % (wins.count(True), len(wins), winRate) + print 'Record: ', ', '.join([ ['Loss', 'Win'][int(w)] for w in wins]) + + return games + +if __name__ == '__main__': + """ + The main function called when pacman.py is run + from the command line: + + > python pacman.py + + See the usage string for more details. + + > python pacman.py --help + """ + args = readCommand( sys.argv[1:] ) # Get game components based on input + runGames( **args ) + + # import cProfile + # cProfile.run("runGames( **args )") + pass diff --git a/pacmanAgents.py b/pacmanAgents.py new file mode 100644 index 0000000..ae97634 --- /dev/null +++ b/pacmanAgents.py @@ -0,0 +1,52 @@ +# pacmanAgents.py +# --------------- +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). + + +from pacman import Directions +from game import Agent +import random +import game +import util + +class LeftTurnAgent(game.Agent): + "An agent that turns left at every opportunity" + + def getAction(self, state): + legal = state.getLegalPacmanActions() + current = state.getPacmanState().configuration.direction + if current == Directions.STOP: current = Directions.NORTH + left = Directions.LEFT[current] + if left in legal: return left + if current in legal: return current + if Directions.RIGHT[current] in legal: return Directions.RIGHT[current] + if Directions.LEFT[left] in legal: return Directions.LEFT[left] + return Directions.STOP + +class GreedyAgent(Agent): + def __init__(self, evalFn="scoreEvaluation"): + self.evaluationFunction = util.lookup(evalFn, globals()) + assert self.evaluationFunction != None + + def getAction(self, state): + # Generate candidate actions + legal = state.getLegalPacmanActions() + if Directions.STOP in legal: legal.remove(Directions.STOP) + + successors = [(state.generateSuccessor(0, action), action) for action in legal] + scored = [(self.evaluationFunction(state), action) for state, action in successors] + bestScore = max(scored)[0] + bestActions = [pair[1] for pair in scored if pair[0] == bestScore] + return random.choice(bestActions) + +def scoreEvaluation(state): + return state.getScore() diff --git a/projectParams.py b/projectParams.py new file mode 100644 index 0000000..dc3e9d1 --- /dev/null +++ b/projectParams.py @@ -0,0 +1,18 @@ +# projectParams.py +# ---------------- +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). + + +STUDENT_CODE_DEFAULT = 'searchAgents.py,search.py' +PROJECT_TEST_CLASSES = 'searchTestClasses.py' +PROJECT_NAME = 'Project 1: Search' +BONUS_PIC = False diff --git a/search.py b/search.py new file mode 100644 index 0000000..d1fa4e7 --- /dev/null +++ b/search.py @@ -0,0 +1,119 @@ +# search.py +# --------- +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). + + +""" +In search.py, you will implement generic search algorithms which are called by +Pacman agents (in searchAgents.py). +""" + +import util + +class SearchProblem: + """ + This class outlines the structure of a search problem, but doesn't implement + any of the methods (in object-oriented terminology: an abstract class). + + You do not need to change anything in this class, ever. + """ + + def getStartState(self): + """ + Returns the start state for the search problem. + """ + util.raiseNotDefined() + + def isGoalState(self, state): + """ + state: Search state + + Returns True if and only if the state is a valid goal state. + """ + util.raiseNotDefined() + + def getSuccessors(self, state): + """ + state: Search state + + For a given state, this should return a list of triples, (successor, + action, stepCost), where 'successor' is a successor to the current + state, 'action' is the action required to get there, and 'stepCost' is + the incremental cost of expanding to that successor. + """ + util.raiseNotDefined() + + def getCostOfActions(self, actions): + """ + actions: A list of actions to take + + This method returns the total cost of a particular sequence of actions. + The sequence must be composed of legal moves. + """ + util.raiseNotDefined() + + +def tinyMazeSearch(problem): + """ + Returns a sequence of moves that solves tinyMaze. For any other maze, the + sequence of moves will be incorrect, so only use this for tinyMaze. + """ + from game import Directions + s = Directions.SOUTH + w = Directions.WEST + return [s, s, w, s, w, w, s, w] + +def depthFirstSearch(problem): + """ + Search the deepest nodes in the search tree first. + + Your search algorithm needs to return a list of actions that reaches the + goal. Make sure to implement a graph search algorithm. + + To get started, you might want to try some of these simple commands to + understand the search problem that is being passed in: + + print "Start:", problem.getStartState() + print "Is the start a goal?", problem.isGoalState(problem.getStartState()) + print "Start's successors:", problem.getSuccessors(problem.getStartState()) + """ + "*** YOUR CODE HERE ***" + util.raiseNotDefined() + +def breadthFirstSearch(problem): + """Search the shallowest nodes in the search tree first.""" + "*** YOUR CODE HERE ***" + util.raiseNotDefined() + +def uniformCostSearch(problem): + """Search the node of least total cost first.""" + "*** YOUR CODE HERE ***" + util.raiseNotDefined() + +def nullHeuristic(state, problem=None): + """ + A heuristic function estimates the cost from the current state to the nearest + goal in the provided SearchProblem. This heuristic is trivial. + """ + return 0 + +def aStarSearch(problem, heuristic=nullHeuristic): + """Search the node that has the lowest combined cost and heuristic first.""" + "*** YOUR CODE HERE ***" + util.raiseNotDefined() + + +# Abbreviations +bfs = breadthFirstSearch +dfs = depthFirstSearch +astar = aStarSearch +ucs = uniformCostSearch diff --git a/searchAgents.py b/searchAgents.py new file mode 100644 index 0000000..1d757b1 --- /dev/null +++ b/searchAgents.py @@ -0,0 +1,542 @@ +# searchAgents.py +# --------------- +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). + + +""" +This file contains all of the agents that can be selected to control Pacman. To +select an agent, use the '-p' option when running pacman.py. Arguments can be +passed to your agent using '-a'. For example, to load a SearchAgent that uses +depth first search (dfs), run the following command: + +> python pacman.py -p SearchAgent -a fn=depthFirstSearch + +Commands to invoke other search strategies can be found in the project +description. + +Please only change the parts of the file you are asked to. Look for the lines +that say + +"*** YOUR CODE HERE ***" + +The parts you fill in start about 3/4 of the way down. Follow the project +description for details. + +Good luck and happy searching! +""" + +from game import Directions +from game import Agent +from game import Actions +import util +import time +import search + +class GoWestAgent(Agent): + "An agent that goes West until it can't." + + def getAction(self, state): + "The agent receives a GameState (defined in pacman.py)." + if Directions.WEST in state.getLegalPacmanActions(): + return Directions.WEST + else: + return Directions.STOP + +####################################################### +# This portion is written for you, but will only work # +# after you fill in parts of search.py # +####################################################### + +class SearchAgent(Agent): + """ + This very general search agent finds a path using a supplied search + algorithm for a supplied search problem, then returns actions to follow that + path. + + As a default, this agent runs DFS on a PositionSearchProblem to find + location (1,1) + + Options for fn include: + depthFirstSearch or dfs + breadthFirstSearch or bfs + + + Note: You should NOT change any code in SearchAgent + """ + + def __init__(self, fn='depthFirstSearch', prob='PositionSearchProblem', heuristic='nullHeuristic'): + # Warning: some advanced Python magic is employed below to find the right functions and problems + + # Get the search function from the name and heuristic + if fn not in dir(search): + raise AttributeError, fn + ' is not a search function in search.py.' + func = getattr(search, fn) + if 'heuristic' not in func.func_code.co_varnames: + print('[SearchAgent] using function ' + fn) + self.searchFunction = func + else: + if heuristic in globals().keys(): + heur = globals()[heuristic] + elif heuristic in dir(search): + heur = getattr(search, heuristic) + else: + raise AttributeError, heuristic + ' is not a function in searchAgents.py or search.py.' + print('[SearchAgent] using function %s and heuristic %s' % (fn, heuristic)) + # Note: this bit of Python trickery combines the search algorithm and the heuristic + self.searchFunction = lambda x: func(x, heuristic=heur) + + # Get the search problem type from the name + if prob not in globals().keys() or not prob.endswith('Problem'): + raise AttributeError, prob + ' is not a search problem type in SearchAgents.py.' + self.searchType = globals()[prob] + print('[SearchAgent] using problem type ' + prob) + + def registerInitialState(self, state): + """ + This is the first time that the agent sees the layout of the game + board. Here, we choose a path to the goal. In this phase, the agent + should compute the path to the goal and store it in a local variable. + All of the work is done in this method! + + state: a GameState object (pacman.py) + """ + if self.searchFunction == None: raise Exception, "No search function provided for SearchAgent" + starttime = time.time() + problem = self.searchType(state) # Makes a new search problem + self.actions = self.searchFunction(problem) # Find a path + totalCost = problem.getCostOfActions(self.actions) + print('Path found with total cost of %d in %.1f seconds' % (totalCost, time.time() - starttime)) + if '_expanded' in dir(problem): print('Search nodes expanded: %d' % problem._expanded) + + def getAction(self, state): + """ + Returns the next action in the path chosen earlier (in + registerInitialState). Return Directions.STOP if there is no further + action to take. + + state: a GameState object (pacman.py) + """ + if 'actionIndex' not in dir(self): self.actionIndex = 0 + i = self.actionIndex + self.actionIndex += 1 + if i < len(self.actions): + return self.actions[i] + else: + return Directions.STOP + +class PositionSearchProblem(search.SearchProblem): + """ + A search problem defines the state space, start state, goal test, successor + function and cost function. This search problem can be used to find paths + to a particular point on the pacman board. + + The state space consists of (x,y) positions in a pacman game. + + Note: this search problem is fully specified; you should NOT change it. + """ + + def __init__(self, gameState, costFn = lambda x: 1, goal=(1,1), start=None, warn=True, visualize=True): + """ + Stores the start and goal. + + gameState: A GameState object (pacman.py) + costFn: A function from a search state (tuple) to a non-negative number + goal: A position in the gameState + """ + self.walls = gameState.getWalls() + self.startState = gameState.getPacmanPosition() + if start != None: self.startState = start + self.goal = goal + self.costFn = costFn + self.visualize = visualize + if warn and (gameState.getNumFood() != 1 or not gameState.hasFood(*goal)): + print 'Warning: this does not look like a regular search maze' + + # For display purposes + self._visited, self._visitedlist, self._expanded = {}, [], 0 # DO NOT CHANGE + + def getStartState(self): + return self.startState + + def isGoalState(self, state): + isGoal = state == self.goal + + # For display purposes only + if isGoal and self.visualize: + self._visitedlist.append(state) + import __main__ + if '_display' in dir(__main__): + if 'drawExpandedCells' in dir(__main__._display): #@UndefinedVariable + __main__._display.drawExpandedCells(self._visitedlist) #@UndefinedVariable + + return isGoal + + def getSuccessors(self, state): + """ + Returns successor states, the actions they require, and a cost of 1. + + As noted in search.py: + For a given state, this should return a list of triples, + (successor, action, stepCost), where 'successor' is a + successor to the current state, 'action' is the action + required to get there, and 'stepCost' is the incremental + cost of expanding to that successor + """ + + successors = [] + for action in [Directions.NORTH, Directions.SOUTH, Directions.EAST, Directions.WEST]: + x,y = state + dx, dy = Actions.directionToVector(action) + nextx, nexty = int(x + dx), int(y + dy) + if not self.walls[nextx][nexty]: + nextState = (nextx, nexty) + cost = self.costFn(nextState) + successors.append( ( nextState, action, cost) ) + + # Bookkeeping for display purposes + self._expanded += 1 # DO NOT CHANGE + if state not in self._visited: + self._visited[state] = True + self._visitedlist.append(state) + + return successors + + def getCostOfActions(self, actions): + """ + Returns the cost of a particular sequence of actions. If those actions + include an illegal move, return 999999. + """ + if actions == None: return 999999 + x,y= self.getStartState() + cost = 0 + for action in actions: + # Check figure out the next state and see whether its' legal + dx, dy = Actions.directionToVector(action) + x, y = int(x + dx), int(y + dy) + if self.walls[x][y]: return 999999 + cost += self.costFn((x,y)) + return cost + +class StayEastSearchAgent(SearchAgent): + """ + An agent for position search with a cost function that penalizes being in + positions on the West side of the board. + + The cost function for stepping into a position (x,y) is 1/2^x. + """ + def __init__(self): + self.searchFunction = search.uniformCostSearch + costFn = lambda pos: .5 ** pos[0] + self.searchType = lambda state: PositionSearchProblem(state, costFn, (1, 1), None, False) + +class StayWestSearchAgent(SearchAgent): + """ + An agent for position search with a cost function that penalizes being in + positions on the East side of the board. + + The cost function for stepping into a position (x,y) is 2^x. + """ + def __init__(self): + self.searchFunction = search.uniformCostSearch + costFn = lambda pos: 2 ** pos[0] + self.searchType = lambda state: PositionSearchProblem(state, costFn) + +def manhattanHeuristic(position, problem, info={}): + "The Manhattan distance heuristic for a PositionSearchProblem" + xy1 = position + xy2 = problem.goal + return abs(xy1[0] - xy2[0]) + abs(xy1[1] - xy2[1]) + +def euclideanHeuristic(position, problem, info={}): + "The Euclidean distance heuristic for a PositionSearchProblem" + xy1 = position + xy2 = problem.goal + return ( (xy1[0] - xy2[0]) ** 2 + (xy1[1] - xy2[1]) ** 2 ) ** 0.5 + +##################################################### +# This portion is incomplete. Time to write code! # +##################################################### + +class CornersProblem(search.SearchProblem): + """ + This search problem finds paths through all four corners of a layout. + + You must select a suitable state space and successor function + """ + + def __init__(self, startingGameState): + """ + Stores the walls, pacman's starting position and corners. + """ + self.walls = startingGameState.getWalls() + self.startingPosition = startingGameState.getPacmanPosition() + top, right = self.walls.height-2, self.walls.width-2 + self.corners = ((1,1), (1,top), (right, 1), (right, top)) + for corner in self.corners: + if not startingGameState.hasFood(*corner): + print 'Warning: no food in corner ' + str(corner) + self._expanded = 0 # DO NOT CHANGE; Number of search nodes expanded + # Please add any code here which you would like to use + # in initializing the problem + "*** YOUR CODE HERE ***" + + def getStartState(self): + """ + Returns the start state (in your state space, not the full Pacman state + space) + """ + "*** YOUR CODE HERE ***" + util.raiseNotDefined() + + def isGoalState(self, state): + """ + Returns whether this search state is a goal state of the problem. + """ + "*** YOUR CODE HERE ***" + util.raiseNotDefined() + + def getSuccessors(self, state): + """ + Returns successor states, the actions they require, and a cost of 1. + + As noted in search.py: + For a given state, this should return a list of triples, (successor, + action, stepCost), where 'successor' is a successor to the current + state, 'action' is the action required to get there, and 'stepCost' + is the incremental cost of expanding to that successor + """ + + successors = [] + for action in [Directions.NORTH, Directions.SOUTH, Directions.EAST, Directions.WEST]: + # Add a successor state to the successor list if the action is legal + # Here's a code snippet for figuring out whether a new position hits a wall: + # x,y = currentPosition + # dx, dy = Actions.directionToVector(action) + # nextx, nexty = int(x + dx), int(y + dy) + # hitsWall = self.walls[nextx][nexty] + + "*** YOUR CODE HERE ***" + + self._expanded += 1 # DO NOT CHANGE + return successors + + def getCostOfActions(self, actions): + """ + Returns the cost of a particular sequence of actions. If those actions + include an illegal move, return 999999. This is implemented for you. + """ + if actions == None: return 999999 + x,y= self.startingPosition + for action in actions: + dx, dy = Actions.directionToVector(action) + x, y = int(x + dx), int(y + dy) + if self.walls[x][y]: return 999999 + return len(actions) + + +def cornersHeuristic(state, problem): + """ + A heuristic for the CornersProblem that you defined. + + state: The current search state + (a data structure you chose in your search problem) + + problem: The CornersProblem instance for this layout. + + This function should always return a number that is a lower bound on the + shortest path from the state to a goal of the problem; i.e. it should be + admissible (as well as consistent). + """ + corners = problem.corners # These are the corner coordinates + walls = problem.walls # These are the walls of the maze, as a Grid (game.py) + + "*** YOUR CODE HERE ***" + return 0 # Default to trivial solution + +class AStarCornersAgent(SearchAgent): + "A SearchAgent for FoodSearchProblem using A* and your foodHeuristic" + def __init__(self): + self.searchFunction = lambda prob: search.aStarSearch(prob, cornersHeuristic) + self.searchType = CornersProblem + +class FoodSearchProblem: + """ + A search problem associated with finding the a path that collects all of the + food (dots) in a Pacman game. + + A search state in this problem is a tuple ( pacmanPosition, foodGrid ) where + pacmanPosition: a tuple (x,y) of integers specifying Pacman's position + foodGrid: a Grid (see game.py) of either True or False, specifying remaining food + """ + def __init__(self, startingGameState): + self.start = (startingGameState.getPacmanPosition(), startingGameState.getFood()) + self.walls = startingGameState.getWalls() + self.startingGameState = startingGameState + self._expanded = 0 # DO NOT CHANGE + self.heuristicInfo = {} # A dictionary for the heuristic to store information + + def getStartState(self): + return self.start + + def isGoalState(self, state): + return state[1].count() == 0 + + def getSuccessors(self, state): + "Returns successor states, the actions they require, and a cost of 1." + successors = [] + self._expanded += 1 # DO NOT CHANGE + for direction in [Directions.NORTH, Directions.SOUTH, Directions.EAST, Directions.WEST]: + x,y = state[0] + dx, dy = Actions.directionToVector(direction) + nextx, nexty = int(x + dx), int(y + dy) + if not self.walls[nextx][nexty]: + nextFood = state[1].copy() + nextFood[nextx][nexty] = False + successors.append( ( ((nextx, nexty), nextFood), direction, 1) ) + return successors + + def getCostOfActions(self, actions): + """Returns the cost of a particular sequence of actions. If those actions + include an illegal move, return 999999""" + x,y= self.getStartState()[0] + cost = 0 + for action in actions: + # figure out the next state and see whether it's legal + dx, dy = Actions.directionToVector(action) + x, y = int(x + dx), int(y + dy) + if self.walls[x][y]: + return 999999 + cost += 1 + return cost + +class AStarFoodSearchAgent(SearchAgent): + "A SearchAgent for FoodSearchProblem using A* and your foodHeuristic" + def __init__(self): + self.searchFunction = lambda prob: search.aStarSearch(prob, foodHeuristic) + self.searchType = FoodSearchProblem + +def foodHeuristic(state, problem): + """ + Your heuristic for the FoodSearchProblem goes here. + + This heuristic must be consistent to ensure correctness. First, try to come + up with an admissible heuristic; almost all admissible heuristics will be + consistent as well. + + If using A* ever finds a solution that is worse uniform cost search finds, + your heuristic is *not* consistent, and probably not admissible! On the + other hand, inadmissible or inconsistent heuristics may find optimal + solutions, so be careful. + + The state is a tuple ( pacmanPosition, foodGrid ) where foodGrid is a Grid + (see game.py) of either True or False. You can call foodGrid.asList() to get + a list of food coordinates instead. + + If you want access to info like walls, capsules, etc., you can query the + problem. For example, problem.walls gives you a Grid of where the walls + are. + + If you want to *store* information to be reused in other calls to the + heuristic, there is a dictionary called problem.heuristicInfo that you can + use. For example, if you only want to count the walls once and store that + value, try: problem.heuristicInfo['wallCount'] = problem.walls.count() + Subsequent calls to this heuristic can access + problem.heuristicInfo['wallCount'] + """ + position, foodGrid = state + "*** YOUR CODE HERE ***" + return 0 + +class ClosestDotSearchAgent(SearchAgent): + "Search for all food using a sequence of searches" + def registerInitialState(self, state): + self.actions = [] + currentState = state + while(currentState.getFood().count() > 0): + nextPathSegment = self.findPathToClosestDot(currentState) # The missing piece + self.actions += nextPathSegment + for action in nextPathSegment: + legal = currentState.getLegalActions() + if action not in legal: + t = (str(action), str(currentState)) + raise Exception, 'findPathToClosestDot returned an illegal move: %s!\n%s' % t + currentState = currentState.generateSuccessor(0, action) + self.actionIndex = 0 + print 'Path found with cost %d.' % len(self.actions) + + def findPathToClosestDot(self, gameState): + """ + Returns a path (a list of actions) to the closest dot, starting from + gameState. + """ + # Here are some useful elements of the startState + startPosition = gameState.getPacmanPosition() + food = gameState.getFood() + walls = gameState.getWalls() + problem = AnyFoodSearchProblem(gameState) + + "*** YOUR CODE HERE ***" + util.raiseNotDefined() + +class AnyFoodSearchProblem(PositionSearchProblem): + """ + A search problem for finding a path to any food. + + This search problem is just like the PositionSearchProblem, but has a + different goal test, which you need to fill in below. The state space and + successor function do not need to be changed. + + The class definition above, AnyFoodSearchProblem(PositionSearchProblem), + inherits the methods of the PositionSearchProblem. + + You can use this search problem to help you fill in the findPathToClosestDot + method. + """ + + def __init__(self, gameState): + "Stores information from the gameState. You don't need to change this." + # Store the food for later reference + self.food = gameState.getFood() + + # Store info for the PositionSearchProblem (no need to change this) + self.walls = gameState.getWalls() + self.startState = gameState.getPacmanPosition() + self.costFn = lambda x: 1 + self._visited, self._visitedlist, self._expanded = {}, [], 0 # DO NOT CHANGE + + def isGoalState(self, state): + """ + The state is Pacman's position. Fill this in with a goal test that will + complete the problem definition. + """ + x,y = state + + "*** YOUR CODE HERE ***" + util.raiseNotDefined() + +def mazeDistance(point1, point2, gameState): + """ + Returns the maze distance between any two points, using the search functions + you have already built. The gameState can be any game state -- Pacman's + position in that state is ignored. + + Example usage: mazeDistance( (2,4), (5,6), gameState) + + This might be a useful helper function for your ApproximateSearchAgent. + """ + x1, y1 = point1 + x2, y2 = point2 + walls = gameState.getWalls() + assert not walls[x1][y1], 'point1 is a wall: ' + str(point1) + assert not walls[x2][y2], 'point2 is a wall: ' + str(point2) + prob = PositionSearchProblem(gameState, start=point1, goal=point2, warn=False, visualize=False) + return len(search.bfs(prob)) diff --git a/searchTestClasses.py b/searchTestClasses.py new file mode 100644 index 0000000..1e985a2 --- /dev/null +++ b/searchTestClasses.py @@ -0,0 +1,821 @@ +# searchTestClasses.py +# -------------------- +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). + + +import re +import testClasses +import textwrap + +# import project specific code +import layout +import pacman +from search import SearchProblem + +# helper function for printing solutions in solution files +def wrap_solution(solution): + if type(solution) == type([]): + return '\n'.join(textwrap.wrap(' '.join(solution))) + else: + return str(solution) + + + + +def followAction(state, action, problem): + for successor1, action1, cost1 in problem.getSuccessors(state): + if action == action1: return successor1 + return None + +def followPath(path, problem): + state = problem.getStartState() + states = [state] + for action in path: + state = followAction(state, action, problem) + states.append(state) + return states + +def checkSolution(problem, path): + state = problem.getStartState() + for action in path: + state = followAction(state, action, problem) + return problem.isGoalState(state) + +# Search problem on a plain graph +class GraphSearch(SearchProblem): + + # Read in the state graph; define start/end states, edges and costs + def __init__(self, graph_text): + self.expanded_states = [] + lines = graph_text.split('\n') + r = re.match('start_state:(.*)', lines[0]) + if r == None: + print "Broken graph:" + print '"""%s"""' % graph_text + raise Exception("GraphSearch graph specification start_state not found or incorrect on line:" + l) + self.start_state = r.group(1).strip() + r = re.match('goal_states:(.*)', lines[1]) + if r == None: + print "Broken graph:" + print '"""%s"""' % graph_text + raise Exception("GraphSearch graph specification goal_states not found or incorrect on line:" + l) + goals = r.group(1).split() + self.goals = map(str.strip, goals) + self.successors = {} + all_states = set() + self.orderedSuccessorTuples = [] + for l in lines[2:]: + if len(l.split()) == 3: + start, action, next_state = l.split() + cost = 1 + elif len(l.split()) == 4: + start, action, next_state, cost = l.split() + else: + print "Broken graph:" + print '"""%s"""' % graph_text + raise Exception("Invalid line in GraphSearch graph specification on line:" + l) + cost = float(cost) + self.orderedSuccessorTuples.append((start, action, next_state, cost)) + all_states.add(start) + all_states.add(next_state) + if start not in self.successors: + self.successors[start] = [] + self.successors[start].append((next_state, action, cost)) + for s in all_states: + if s not in self.successors: + self.successors[s] = [] + + # Get start state + def getStartState(self): + return self.start_state + + # Check if a state is a goal state + def isGoalState(self, state): + return state in self.goals + + # Get all successors of a state + def getSuccessors(self, state): + self.expanded_states.append(state) + return list(self.successors[state]) + + # Calculate total cost of a sequence of actions + def getCostOfActions(self, actions): + total_cost = 0 + state = self.start_state + for a in actions: + successors = self.successors[state] + match = False + for (next_state, action, cost) in successors: + if a == action: + state = next_state + total_cost += cost + match = True + if not match: + print 'invalid action sequence' + sys.exit(1) + return total_cost + + # Return a list of all states on which 'getSuccessors' was called + def getExpandedStates(self): + return self.expanded_states + + def __str__(self): + print self.successors + edges = ["%s %s %s %s" % t for t in self.orderedSuccessorTuples] + return \ +"""start_state: %s +goal_states: %s +%s""" % (self.start_state, " ".join(self.goals), "\n".join(edges)) + + + +def parseHeuristic(heuristicText): + heuristic = {} + for line in heuristicText.split('\n'): + tokens = line.split() + if len(tokens) != 2: + print "Broken heuristic:" + print '"""%s"""' % graph_text + raise Exception("GraphSearch heuristic specification broken:" + l) + state, h = tokens + heuristic[state] = float(h) + + def graphHeuristic(state, problem=None): + if state in heuristic: + return heuristic[state] + else: + pp = pprint.PrettyPrinter(indent=4) + print "Heuristic:" + pp.pprint(heuristic) + raise Exception("Graph heuristic called with invalid state: " + str(state)) + + return graphHeuristic + + +class GraphSearchTest(testClasses.TestCase): + + def __init__(self, question, testDict): + super(GraphSearchTest, self).__init__(question, testDict) + self.graph_text = testDict['graph'] + self.alg = testDict['algorithm'] + self.diagram = testDict['diagram'] + self.exactExpansionOrder = testDict.get('exactExpansionOrder', 'True').lower() == "true" + if 'heuristic' in testDict: + self.heuristic = parseHeuristic(testDict['heuristic']) + else: + self.heuristic = None + + # Note that the return type of this function is a tripple: + # (solution, expanded states, error message) + def getSolInfo(self, search): + alg = getattr(search, self.alg) + problem = GraphSearch(self.graph_text) + if self.heuristic != None: + solution = alg(problem, self.heuristic) + else: + solution = alg(problem) + + if type(solution) != type([]): + return None, None, 'The result of %s must be a list. (Instead, it is %s)' % (self.alg, type(solution)) + + return solution, problem.getExpandedStates(), None + + # Run student code. If an error message is returned, print error and return false. + # If a good solution is returned, printn the solution and return true; otherwise, + # print both the correct and student's solution and return false. + def execute(self, grades, moduleDict, solutionDict): + search = moduleDict['search'] + searchAgents = moduleDict['searchAgents'] + gold_solution = [str.split(solutionDict['solution']), str.split(solutionDict['rev_solution'])] + gold_expanded_states = [str.split(solutionDict['expanded_states']), str.split(solutionDict['rev_expanded_states'])] + + solution, expanded_states, error = self.getSolInfo(search) + if error != None: + grades.addMessage('FAIL: %s' % self.path) + grades.addMessage('\t%s' % error) + return False + + if solution in gold_solution and (not self.exactExpansionOrder or expanded_states in gold_expanded_states): + grades.addMessage('PASS: %s' % self.path) + grades.addMessage('\tsolution:\t\t%s' % solution) + grades.addMessage('\texpanded_states:\t%s' % expanded_states) + return True + else: + grades.addMessage('FAIL: %s' % self.path) + grades.addMessage('\tgraph:') + for line in self.diagram.split('\n'): + grades.addMessage('\t %s' % (line,)) + grades.addMessage('\tstudent solution:\t\t%s' % solution) + grades.addMessage('\tstudent expanded_states:\t%s' % expanded_states) + grades.addMessage('') + grades.addMessage('\tcorrect solution:\t\t%s' % gold_solution[0]) + grades.addMessage('\tcorrect expanded_states:\t%s' % gold_expanded_states[0]) + grades.addMessage('\tcorrect rev_solution:\t\t%s' % gold_solution[1]) + grades.addMessage('\tcorrect rev_expanded_states:\t%s' % gold_expanded_states[1]) + return False + + def writeSolution(self, moduleDict, filePath): + search = moduleDict['search'] + searchAgents = moduleDict['searchAgents'] + # open file and write comments + handle = open(filePath, 'w') + handle.write('# This is the solution file for %s.\n' % self.path) + handle.write('# This solution is designed to support both right-to-left\n') + handle.write('# and left-to-right implementations.\n') + + # write forward solution + solution, expanded_states, error = self.getSolInfo(search) + if error != None: raise Exception("Error in solution code: %s" % error) + handle.write('solution: "%s"\n' % ' '.join(solution)) + handle.write('expanded_states: "%s"\n' % ' '.join(expanded_states)) + + # reverse and write backwards solution + search.REVERSE_PUSH = not search.REVERSE_PUSH + solution, expanded_states, error = self.getSolInfo(search) + if error != None: raise Exception("Error in solution code: %s" % error) + handle.write('rev_solution: "%s"\n' % ' '.join(solution)) + handle.write('rev_expanded_states: "%s"\n' % ' '.join(expanded_states)) + + # clean up + search.REVERSE_PUSH = not search.REVERSE_PUSH + handle.close() + return True + + + +class PacmanSearchTest(testClasses.TestCase): + + def __init__(self, question, testDict): + super(PacmanSearchTest, self).__init__(question, testDict) + self.layout_text = testDict['layout'] + self.alg = testDict['algorithm'] + self.layoutName = testDict['layoutName'] + + # TODO: sensible to have defaults like this? + self.leewayFactor = float(testDict.get('leewayFactor', '1')) + self.costFn = eval(testDict.get('costFn', 'None')) + self.searchProblemClassName = testDict.get('searchProblemClass', 'PositionSearchProblem') + self.heuristicName = testDict.get('heuristic', None) + + + def getSolInfo(self, search, searchAgents): + alg = getattr(search, self.alg) + lay = layout.Layout([l.strip() for l in self.layout_text.split('\n')]) + start_state = pacman.GameState() + start_state.initialize(lay, 0) + + problemClass = getattr(searchAgents, self.searchProblemClassName) + problemOptions = {} + if self.costFn != None: + problemOptions['costFn'] = self.costFn + problem = problemClass(start_state, **problemOptions) + heuristic = getattr(searchAgents, self.heuristicName) if self.heuristicName != None else None + + if heuristic != None: + solution = alg(problem, heuristic) + else: + solution = alg(problem) + + if type(solution) != type([]): + return None, None, 'The result of %s must be a list. (Instead, it is %s)' % (self.alg, type(solution)) + + from game import Directions + dirs = Directions.LEFT.keys() + if [el in dirs for el in solution].count(False) != 0: + return None, None, 'Output of %s must be a list of actions from game.Directions' % self.alg + + expanded = problem._expanded + return solution, expanded, None + + def execute(self, grades, moduleDict, solutionDict): + search = moduleDict['search'] + searchAgents = moduleDict['searchAgents'] + gold_solution = [str.split(solutionDict['solution']), str.split(solutionDict['rev_solution'])] + gold_expanded = max(int(solutionDict['expanded_nodes']), int(solutionDict['rev_expanded_nodes'])) + + solution, expanded, error = self.getSolInfo(search, searchAgents) + if error != None: + grades.addMessage('FAIL: %s' % self.path) + grades.addMessage('%s' % error) + return False + + # FIXME: do we want to standardize test output format? + + if solution not in gold_solution: + grades.addMessage('FAIL: %s' % self.path) + grades.addMessage('Solution not correct.') + grades.addMessage('\tstudent solution length: %s' % len(solution)) + grades.addMessage('\tstudent solution:\n%s' % wrap_solution(solution)) + grades.addMessage('') + grades.addMessage('\tcorrect solution length: %s' % len(gold_solution[0])) + grades.addMessage('\tcorrect (reversed) solution length: %s' % len(gold_solution[1])) + grades.addMessage('\tcorrect solution:\n%s' % wrap_solution(gold_solution[0])) + grades.addMessage('\tcorrect (reversed) solution:\n%s' % wrap_solution(gold_solution[1])) + return False + + if expanded > self.leewayFactor * gold_expanded and expanded > gold_expanded + 1: + grades.addMessage('FAIL: %s' % self.path) + grades.addMessage('Too many node expanded; are you expanding nodes twice?') + grades.addMessage('\tstudent nodes expanded: %s' % expanded) + grades.addMessage('') + grades.addMessage('\tcorrect nodes expanded: %s (leewayFactor %s)' % (gold_expanded, self.leewayFactor)) + return False + + grades.addMessage('PASS: %s' % self.path) + grades.addMessage('\tpacman layout:\t\t%s' % self.layoutName) + grades.addMessage('\tsolution length: %s' % len(solution)) + grades.addMessage('\tnodes expanded:\t\t%s' % expanded) + return True + + + def writeSolution(self, moduleDict, filePath): + search = moduleDict['search'] + searchAgents = moduleDict['searchAgents'] + # open file and write comments + handle = open(filePath, 'w') + handle.write('# This is the solution file for %s.\n' % self.path) + handle.write('# This solution is designed to support both right-to-left\n') + handle.write('# and left-to-right implementations.\n') + handle.write('# Number of nodes expanded must be with a factor of %s of the numbers below.\n' % self.leewayFactor) + + # write forward solution + solution, expanded, error = self.getSolInfo(search, searchAgents) + if error != None: raise Exception("Error in solution code: %s" % error) + handle.write('solution: """\n%s\n"""\n' % wrap_solution(solution)) + handle.write('expanded_nodes: "%s"\n' % expanded) + + # write backward solution + search.REVERSE_PUSH = not search.REVERSE_PUSH + solution, expanded, error = self.getSolInfo(search, searchAgents) + if error != None: raise Exception("Error in solution code: %s" % error) + handle.write('rev_solution: """\n%s\n"""\n' % wrap_solution(solution)) + handle.write('rev_expanded_nodes: "%s"\n' % expanded) + + # clean up + search.REVERSE_PUSH = not search.REVERSE_PUSH + handle.close() + return True + + +from game import Actions +def getStatesFromPath(start, path): + "Returns the list of states visited along the path" + vis = [start] + curr = start + for a in path: + x,y = curr + dx, dy = Actions.directionToVector(a) + curr = (int(x + dx), int(y + dy)) + vis.append(curr) + return vis + +class CornerProblemTest(testClasses.TestCase): + + def __init__(self, question, testDict): + super(CornerProblemTest, self).__init__(question, testDict) + self.layoutText = testDict['layout'] + self.layoutName = testDict['layoutName'] + + def solution(self, search, searchAgents): + lay = layout.Layout([l.strip() for l in self.layoutText.split('\n')]) + gameState = pacman.GameState() + gameState.initialize(lay, 0) + problem = searchAgents.CornersProblem(gameState) + path = search.bfs(problem) + + gameState = pacman.GameState() + gameState.initialize(lay, 0) + visited = getStatesFromPath(gameState.getPacmanPosition(), path) + top, right = gameState.getWalls().height-2, gameState.getWalls().width-2 + missedCorners = [p for p in ((1,1), (1,top), (right, 1), (right, top)) if p not in visited] + + return path, missedCorners + + def execute(self, grades, moduleDict, solutionDict): + search = moduleDict['search'] + searchAgents = moduleDict['searchAgents'] + gold_length = int(solutionDict['solution_length']) + solution, missedCorners = self.solution(search, searchAgents) + + if type(solution) != type([]): + grades.addMessage('FAIL: %s' % self.path) + grades.addMessage('The result must be a list. (Instead, it is %s)' % type(solution)) + return False + + if len(missedCorners) != 0: + grades.addMessage('FAIL: %s' % self.path) + grades.addMessage('Corners missed: %s' % missedCorners) + return False + + if len(solution) != gold_length: + grades.addMessage('FAIL: %s' % self.path) + grades.addMessage('Optimal solution not found.') + grades.addMessage('\tstudent solution length:\n%s' % len(solution)) + grades.addMessage('') + grades.addMessage('\tcorrect solution length:\n%s' % gold_length) + return False + + grades.addMessage('PASS: %s' % self.path) + grades.addMessage('\tpacman layout:\t\t%s' % self.layoutName) + grades.addMessage('\tsolution length:\t\t%s' % len(solution)) + return True + + def writeSolution(self, moduleDict, filePath): + search = moduleDict['search'] + searchAgents = moduleDict['searchAgents'] + # open file and write comments + handle = open(filePath, 'w') + handle.write('# This is the solution file for %s.\n' % self.path) + + print "Solving problem", self.layoutName + print self.layoutText + + path, _ = self.solution(search, searchAgents) + length = len(path) + print "Problem solved" + + handle.write('solution_length: "%s"\n' % length) + handle.close() + + + + +# template = """class: "HeuristicTest" +# +# heuristic: "foodHeuristic" +# searchProblemClass: "FoodSearchProblem" +# layoutName: "Test %s" +# layout: \"\"\" +# %s +# \"\"\" +# """ +# +# for i, (_, _, l) in enumerate(doneTests + foodTests): +# f = open("food_heuristic_%s.test" % (i+1), "w") +# f.write(template % (i+1, "\n".join(l))) +# f.close() + +class HeuristicTest(testClasses.TestCase): + + def __init__(self, question, testDict): + super(HeuristicTest, self).__init__(question, testDict) + self.layoutText = testDict['layout'] + self.layoutName = testDict['layoutName'] + self.searchProblemClassName = testDict['searchProblemClass'] + self.heuristicName = testDict['heuristic'] + + def setupProblem(self, searchAgents): + lay = layout.Layout([l.strip() for l in self.layoutText.split('\n')]) + gameState = pacman.GameState() + gameState.initialize(lay, 0) + problemClass = getattr(searchAgents, self.searchProblemClassName) + problem = problemClass(gameState) + state = problem.getStartState() + heuristic = getattr(searchAgents, self.heuristicName) + + return problem, state, heuristic + + def checkHeuristic(self, heuristic, problem, state, solutionCost): + h0 = heuristic(state, problem) + + if solutionCost == 0: + if h0 == 0: + return True, '' + else: + return False, 'Heuristic failed H(goal) == 0 test' + + if h0 < 0: + return False, 'Heuristic failed H >= 0 test' + if not h0 > 0: + return False, 'Heuristic failed non-triviality test' + if not h0 <= solutionCost: + return False, 'Heuristic failed admissibility test' + + for succ, action, stepCost in problem.getSuccessors(state): + h1 = heuristic(succ, problem) + if h1 < 0: return False, 'Heuristic failed H >= 0 test' + if h0 - h1 > stepCost: return False, 'Heuristic failed consistency test' + + return True, '' + + def execute(self, grades, moduleDict, solutionDict): + search = moduleDict['search'] + searchAgents = moduleDict['searchAgents'] + solutionCost = int(solutionDict['solution_cost']) + problem, state, heuristic = self.setupProblem(searchAgents) + + passed, message = self.checkHeuristic(heuristic, problem, state, solutionCost) + + if not passed: + grades.addMessage('FAIL: %s' % self.path) + grades.addMessage('%s' % message) + return False + else: + grades.addMessage('PASS: %s' % self.path) + return True + + def writeSolution(self, moduleDict, filePath): + search = moduleDict['search'] + searchAgents = moduleDict['searchAgents'] + # open file and write comments + handle = open(filePath, 'w') + handle.write('# This is the solution file for %s.\n' % self.path) + + print "Solving problem", self.layoutName, self.heuristicName + print self.layoutText + problem, _, heuristic = self.setupProblem(searchAgents) + path = search.astar(problem, heuristic) + cost = problem.getCostOfActions(path) + print "Problem solved" + + handle.write('solution_cost: "%s"\n' % cost) + handle.close() + return True + + + + + + +class HeuristicGrade(testClasses.TestCase): + + def __init__(self, question, testDict): + super(HeuristicGrade, self).__init__(question, testDict) + self.layoutText = testDict['layout'] + self.layoutName = testDict['layoutName'] + self.searchProblemClassName = testDict['searchProblemClass'] + self.heuristicName = testDict['heuristic'] + self.basePoints = int(testDict['basePoints']) + self.thresholds = [int(t) for t in testDict['gradingThresholds'].split()] + + def setupProblem(self, searchAgents): + lay = layout.Layout([l.strip() for l in self.layoutText.split('\n')]) + gameState = pacman.GameState() + gameState.initialize(lay, 0) + problemClass = getattr(searchAgents, self.searchProblemClassName) + problem = problemClass(gameState) + state = problem.getStartState() + heuristic = getattr(searchAgents, self.heuristicName) + + return problem, state, heuristic + + + def execute(self, grades, moduleDict, solutionDict): + search = moduleDict['search'] + searchAgents = moduleDict['searchAgents'] + problem, _, heuristic = self.setupProblem(searchAgents) + + path = search.astar(problem, heuristic) + + expanded = problem._expanded + + if not checkSolution(problem, path): + grades.addMessage('FAIL: %s' % self.path) + grades.addMessage('\tReturned path is not a solution.') + grades.addMessage('\tpath returned by astar: %s' % expanded) + return False + + grades.addPoints(self.basePoints) + points = 0 + for threshold in self.thresholds: + if expanded <= threshold: + points += 1 + grades.addPoints(points) + if points >= len(self.thresholds): + grades.addMessage('PASS: %s' % self.path) + else: + grades.addMessage('FAIL: %s' % self.path) + grades.addMessage('\texpanded nodes: %s' % expanded) + grades.addMessage('\tthresholds: %s' % self.thresholds) + + return True + + + def writeSolution(self, moduleDict, filePath): + handle = open(filePath, 'w') + handle.write('# This is the solution file for %s.\n' % self.path) + handle.write('# File intentionally blank.\n') + handle.close() + return True + + + + + +# template = """class: "ClosestDotTest" +# +# layoutName: "Test %s" +# layout: \"\"\" +# %s +# \"\"\" +# """ +# +# for i, (_, _, l) in enumerate(foodTests): +# f = open("closest_dot_%s.test" % (i+1), "w") +# f.write(template % (i+1, "\n".join(l))) +# f.close() + +class ClosestDotTest(testClasses.TestCase): + + def __init__(self, question, testDict): + super(ClosestDotTest, self).__init__(question, testDict) + self.layoutText = testDict['layout'] + self.layoutName = testDict['layoutName'] + + def solution(self, searchAgents): + lay = layout.Layout([l.strip() for l in self.layoutText.split('\n')]) + gameState = pacman.GameState() + gameState.initialize(lay, 0) + path = searchAgents.ClosestDotSearchAgent().findPathToClosestDot(gameState) + return path + + def execute(self, grades, moduleDict, solutionDict): + search = moduleDict['search'] + searchAgents = moduleDict['searchAgents'] + gold_length = int(solutionDict['solution_length']) + solution = self.solution(searchAgents) + + if type(solution) != type([]): + grades.addMessage('FAIL: %s' % self.path) + grades.addMessage('\tThe result must be a list. (Instead, it is %s)' % type(solution)) + return False + + if len(solution) != gold_length: + grades.addMessage('FAIL: %s' % self.path) + grades.addMessage('Closest dot not found.') + grades.addMessage('\tstudent solution length:\n%s' % len(solution)) + grades.addMessage('') + grades.addMessage('\tcorrect solution length:\n%s' % gold_length) + return False + + grades.addMessage('PASS: %s' % self.path) + grades.addMessage('\tpacman layout:\t\t%s' % self.layoutName) + grades.addMessage('\tsolution length:\t\t%s' % len(solution)) + return True + + def writeSolution(self, moduleDict, filePath): + search = moduleDict['search'] + searchAgents = moduleDict['searchAgents'] + # open file and write comments + handle = open(filePath, 'w') + handle.write('# This is the solution file for %s.\n' % self.path) + + print "Solving problem", self.layoutName + print self.layoutText + + length = len(self.solution(searchAgents)) + print "Problem solved" + + handle.write('solution_length: "%s"\n' % length) + handle.close() + return True + + + + +class CornerHeuristicSanity(testClasses.TestCase): + + def __init__(self, question, testDict): + super(CornerHeuristicSanity, self).__init__(question, testDict) + self.layout_text = testDict['layout'] + + def execute(self, grades, moduleDict, solutionDict): + search = moduleDict['search'] + searchAgents = moduleDict['searchAgents'] + game_state = pacman.GameState() + lay = layout.Layout([l.strip() for l in self.layout_text.split('\n')]) + game_state.initialize(lay, 0) + problem = searchAgents.CornersProblem(game_state) + start_state = problem.getStartState() + h0 = searchAgents.cornersHeuristic(start_state, problem) + succs = problem.getSuccessors(start_state) + # cornerConsistencyA + for succ in succs: + h1 = searchAgents.cornersHeuristic(succ[0], problem) + if h0 - h1 > 1: + grades.addMessage('FAIL: inconsistent heuristic') + return False + heuristic_cost = searchAgents.cornersHeuristic(start_state, problem) + true_cost = float(solutionDict['cost']) + # cornerNontrivial + if heuristic_cost == 0: + grades.addMessage('FAIL: must use non-trivial heuristic') + return False + # cornerAdmissible + if heuristic_cost > true_cost: + grades.addMessage('FAIL: Inadmissible heuristic') + return False + path = solutionDict['path'].split() + states = followPath(path, problem) + heuristics = [] + for state in states: + heuristics.append(searchAgents.cornersHeuristic(state, problem)) + for i in range(0, len(heuristics) - 1): + h0 = heuristics[i] + h1 = heuristics[i+1] + # cornerConsistencyB + if h0 - h1 > 1: + grades.addMessage('FAIL: inconsistent heuristic') + return False + # cornerPosH + if h0 < 0 or h1 <0: + grades.addMessage('FAIL: non-positive heuristic') + return False + # cornerGoalH + if heuristics[len(heuristics) - 1] != 0: + grades.addMessage('FAIL: heuristic non-zero at goal') + return False + grades.addMessage('PASS: heuristic value less than true cost at start state') + return True + + def writeSolution(self, moduleDict, filePath): + search = moduleDict['search'] + searchAgents = moduleDict['searchAgents'] + # write comment + handle = open(filePath, 'w') + handle.write('# In order for a heuristic to be admissible, the value\n') + handle.write('# of the heuristic must be less at each state than the\n') + handle.write('# true cost of the optimal path from that state to a goal.\n') + + # solve problem and write solution + lay = layout.Layout([l.strip() for l in self.layout_text.split('\n')]) + start_state = pacman.GameState() + start_state.initialize(lay, 0) + problem = searchAgents.CornersProblem(start_state) + solution = search.astar(problem, searchAgents.cornersHeuristic) + handle.write('cost: "%d"\n' % len(solution)) + handle.write('path: """\n%s\n"""\n' % wrap_solution(solution)) + handle.close() + return True + + + +class CornerHeuristicPacman(testClasses.TestCase): + + def __init__(self, question, testDict): + super(CornerHeuristicPacman, self).__init__(question, testDict) + self.layout_text = testDict['layout'] + + def execute(self, grades, moduleDict, solutionDict): + search = moduleDict['search'] + searchAgents = moduleDict['searchAgents'] + total = 0 + true_cost = float(solutionDict['cost']) + thresholds = map(int, solutionDict['thresholds'].split()) + game_state = pacman.GameState() + lay = layout.Layout([l.strip() for l in self.layout_text.split('\n')]) + game_state.initialize(lay, 0) + problem = searchAgents.CornersProblem(game_state) + start_state = problem.getStartState() + if searchAgents.cornersHeuristic(start_state, problem) > true_cost: + grades.addMessage('FAIL: Inadmissible heuristic') + return False + path = search.astar(problem, searchAgents.cornersHeuristic) + print "path:", path + print "path length:", len(path) + cost = problem.getCostOfActions(path) + if cost > true_cost: + grades.addMessage('FAIL: Inconsistent heuristic') + return False + expanded = problem._expanded + points = 0 + for threshold in thresholds: + if expanded <= threshold: + points += 1 + grades.addPoints(points) + if points >= len(thresholds): + grades.addMessage('PASS: Heuristic resulted in expansion of %d nodes' % expanded) + else: + grades.addMessage('FAIL: Heuristic resulted in expansion of %d nodes' % expanded) + return True + + def writeSolution(self, moduleDict, filePath): + search = moduleDict['search'] + searchAgents = moduleDict['searchAgents'] + # write comment + handle = open(filePath, 'w') + handle.write('# This solution file specifies the length of the optimal path\n') + handle.write('# as well as the thresholds on number of nodes expanded to be\n') + handle.write('# used in scoring.\n') + + # solve problem and write solution + lay = layout.Layout([l.strip() for l in self.layout_text.split('\n')]) + start_state = pacman.GameState() + start_state.initialize(lay, 0) + problem = searchAgents.CornersProblem(start_state) + solution = search.astar(problem, searchAgents.cornersHeuristic) + handle.write('cost: "%d"\n' % len(solution)) + handle.write('path: """\n%s\n"""\n' % wrap_solution(solution)) + handle.write('thresholds: "2000 1600 1200"\n') + handle.close() + return True + diff --git a/submission_autograder.py b/submission_autograder.py new file mode 100644 index 0000000..a521913 --- /dev/null +++ b/submission_autograder.py @@ -0,0 +1,41 @@ +# submission_autograder.py +# ------------------------ +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). + + +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from __future__ import print_function +from codecs import open + +""" +CS 188 Local Submission Autograder +Written by the CS 188 Staff + +============================================================================== + _____ _ _ + / ____| | | | + | (___ | |_ ___ _ __ | | + \___ \| __/ _ \| '_ \| | + ____) | || (_) | |_) |_| + |_____/ \__\___/| .__/(_) + | | + |_| + +Modifying or tampering with this file is a violation of course policy. +If you're having trouble running the autograder, please contact the staff. +============================================================================== +""" +import bz2, base64 +exec(bz2.decompress(base64.b64decode('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'))) + diff --git a/testClasses.py b/testClasses.py new file mode 100644 index 0000000..6f95533 --- /dev/null +++ b/testClasses.py @@ -0,0 +1,206 @@ +# testClasses.py +# -------------- +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). + + +# import modules from python standard library +import inspect +import re +import sys + + +# Class which models a question in a project. Note that questions have a +# maximum number of points they are worth, and are composed of a series of +# test cases +class Question(object): + + def raiseNotDefined(self): + print 'Method not implemented: %s' % inspect.stack()[1][3] + sys.exit(1) + + def __init__(self, questionDict, display): + self.maxPoints = int(questionDict['max_points']) + self.testCases = [] + self.display = display + + def getDisplay(self): + return self.display + + def getMaxPoints(self): + return self.maxPoints + + # Note that 'thunk' must be a function which accepts a single argument, + # namely a 'grading' object + def addTestCase(self, testCase, thunk): + self.testCases.append((testCase, thunk)) + + def execute(self, grades): + self.raiseNotDefined() + +# Question in which all test cases must be passed in order to receive credit +class PassAllTestsQuestion(Question): + + def execute(self, grades): + # TODO: is this the right way to use grades? The autograder doesn't seem to use it. + testsFailed = False + grades.assignZeroCredit() + for _, f in self.testCases: + if not f(grades): + testsFailed = True + if testsFailed: + grades.fail("Tests failed.") + else: + grades.assignFullCredit() + +class ExtraCreditPassAllTestsQuestion(Question): + def __init__(self, questionDict, display): + Question.__init__(self, questionDict, display) + self.extraPoints = int(questionDict['extra_points']) + + def execute(self, grades): + # TODO: is this the right way to use grades? The autograder doesn't seem to use it. + testsFailed = False + grades.assignZeroCredit() + for _, f in self.testCases: + if not f(grades): + testsFailed = True + if testsFailed: + grades.fail("Tests failed.") + else: + grades.assignFullCredit() + grades.addPoints(self.extraPoints) + +# Question in which predict credit is given for test cases with a ``points'' property. +# All other tests are mandatory and must be passed. +class HackedPartialCreditQuestion(Question): + + def execute(self, grades): + # TODO: is this the right way to use grades? The autograder doesn't seem to use it. + grades.assignZeroCredit() + + points = 0 + passed = True + for testCase, f in self.testCases: + testResult = f(grades) + if "points" in testCase.testDict: + if testResult: points += float(testCase.testDict["points"]) + else: + passed = passed and testResult + + ## FIXME: Below terrible hack to match q3's logic + if int(points) == self.maxPoints and not passed: + grades.assignZeroCredit() + else: + grades.addPoints(int(points)) + + +class Q6PartialCreditQuestion(Question): + """Fails any test which returns False, otherwise doesn't effect the grades object. + Partial credit tests will add the required points.""" + + def execute(self, grades): + grades.assignZeroCredit() + + results = [] + for _, f in self.testCases: + results.append(f(grades)) + if False in results: + grades.assignZeroCredit() + +class PartialCreditQuestion(Question): + """Fails any test which returns False, otherwise doesn't effect the grades object. + Partial credit tests will add the required points.""" + + def execute(self, grades): + grades.assignZeroCredit() + + for _, f in self.testCases: + if not f(grades): + grades.assignZeroCredit() + grades.fail("Tests failed.") + return False + + + +class NumberPassedQuestion(Question): + """Grade is the number of test cases passed.""" + + def execute(self, grades): + grades.addPoints([f(grades) for _, f in self.testCases].count(True)) + + + + + +# Template modeling a generic test case +class TestCase(object): + + def raiseNotDefined(self): + print 'Method not implemented: %s' % inspect.stack()[1][3] + sys.exit(1) + + def getPath(self): + return self.path + + def __init__(self, question, testDict): + self.question = question + self.testDict = testDict + self.path = testDict['path'] + self.messages = [] + + def __str__(self): + self.raiseNotDefined() + + def execute(self, grades, moduleDict, solutionDict): + self.raiseNotDefined() + + def writeSolution(self, moduleDict, filePath): + self.raiseNotDefined() + return True + + # Tests should call the following messages for grading + # to ensure a uniform format for test output. + # + # TODO: this is hairy, but we need to fix grading.py's interface + # to get a nice hierarchical project - question - test structure, + # then these should be moved into Question proper. + def testPass(self, grades): + grades.addMessage('PASS: %s' % (self.path,)) + for line in self.messages: + grades.addMessage(' %s' % (line,)) + return True + + def testFail(self, grades): + grades.addMessage('FAIL: %s' % (self.path,)) + for line in self.messages: + grades.addMessage(' %s' % (line,)) + return False + + # This should really be question level? + # + def testPartial(self, grades, points, maxPoints): + grades.addPoints(points) + extraCredit = max(0, points - maxPoints) + regularCredit = points - extraCredit + + grades.addMessage('%s: %s (%s of %s points)' % ("PASS" if points >= maxPoints else "FAIL", self.path, regularCredit, maxPoints)) + if extraCredit > 0: + grades.addMessage('EXTRA CREDIT: %s points' % (extraCredit,)) + + for line in self.messages: + grades.addMessage(' %s' % (line,)) + + return True + + def addMessage(self, message): + self.messages.extend(message.split('\n')) + diff --git a/testParser.py b/testParser.py new file mode 100644 index 0000000..ceedeaf --- /dev/null +++ b/testParser.py @@ -0,0 +1,85 @@ +# testParser.py +# ------------- +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). + + +import re +import sys + +class TestParser(object): + + def __init__(self, path): + # save the path to the test file + self.path = path + + def removeComments(self, rawlines): + # remove any portion of a line following a '#' symbol + fixed_lines = [] + for l in rawlines: + idx = l.find('#') + if idx == -1: + fixed_lines.append(l) + else: + fixed_lines.append(l[0:idx]) + return '\n'.join(fixed_lines) + + def parse(self): + # read in the test case and remove comments + test = {} + with open(self.path) as handle: + raw_lines = handle.read().split('\n') + + test_text = self.removeComments(raw_lines) + test['__raw_lines__'] = raw_lines + test['path'] = self.path + test['__emit__'] = [] + lines = test_text.split('\n') + i = 0 + # read a property in each loop cycle + while(i < len(lines)): + # skip blank lines + if re.match('\A\s*\Z', lines[i]): + test['__emit__'].append(("raw", raw_lines[i])) + i += 1 + continue + m = re.match('\A([^"]*?):\s*"([^"]*)"\s*\Z', lines[i]) + if m: + test[m.group(1)] = m.group(2) + test['__emit__'].append(("oneline", m.group(1))) + i += 1 + continue + m = re.match('\A([^"]*?):\s*"""\s*\Z', lines[i]) + if m: + msg = [] + i += 1 + while(not re.match('\A\s*"""\s*\Z', lines[i])): + msg.append(raw_lines[i]) + i += 1 + test[m.group(1)] = '\n'.join(msg) + test['__emit__'].append(("multiline", m.group(1))) + i += 1 + continue + print 'error parsing test file: %s' % self.path + sys.exit(1) + return test + + +def emitTestDict(testDict, handle): + for kind, data in testDict['__emit__']: + if kind == "raw": + handle.write(data + "\n") + elif kind == "oneline": + handle.write('%s: "%s"\n' % (data, testDict[data])) + elif kind == "multiline": + handle.write('%s: """\n%s\n"""\n' % (data, testDict[data])) + else: + raise Exception("Bad __emit__") diff --git a/test_cases/CONFIG b/test_cases/CONFIG new file mode 100644 index 0000000..dbed66b --- /dev/null +++ b/test_cases/CONFIG @@ -0,0 +1 @@ +order: "q1 q2 q3 q4 q5 q6 q7 q8" \ No newline at end of file diff --git a/test_cases/q1/CONFIG b/test_cases/q1/CONFIG new file mode 100644 index 0000000..ad7e38a --- /dev/null +++ b/test_cases/q1/CONFIG @@ -0,0 +1,2 @@ +max_points: "3" +class: "PassAllTestsQuestion" diff --git a/test_cases/q1/graph_backtrack.solution b/test_cases/q1/graph_backtrack.solution new file mode 100644 index 0000000..c52850c --- /dev/null +++ b/test_cases/q1/graph_backtrack.solution @@ -0,0 +1,7 @@ +# This is the solution file for test_cases/q1/graph_backtrack.test. +# This solution is designed to support both right-to-left +# and left-to-right implementations. +solution: "1:A->C 0:C->G" +expanded_states: "A D C" +rev_solution: "1:A->C 0:C->G" +rev_expanded_states: "A B C" diff --git a/test_cases/q1/graph_backtrack.test b/test_cases/q1/graph_backtrack.test new file mode 100644 index 0000000..05640a0 --- /dev/null +++ b/test_cases/q1/graph_backtrack.test @@ -0,0 +1,32 @@ +class: "GraphSearchTest" +algorithm: "depthFirstSearch" + +diagram: """ + B + ^ + | +*A --> C --> G + | + V + D + +A is the start state, G is the goal. Arrows mark +possible state transitions. This tests whether +you extract the sequence of actions correctly even +if your search backtracks. If you fail this, your +nodes are not correctly tracking the sequences of +actions required to reach them. +""" +# The following section specifies the search problem and the solution. +# The graph is specified by first the set of start states, followed by +# the set of goal states, and lastly by the state transitions which are +# of the form: +# +graph: """ +start_state: A +goal_states: G +A 0:A->B B 1.0 +A 1:A->C C 2.0 +A 2:A->D D 4.0 +C 0:C->G G 8.0 +""" diff --git a/test_cases/q1/graph_bfs_vs_dfs.solution b/test_cases/q1/graph_bfs_vs_dfs.solution new file mode 100644 index 0000000..0680f92 --- /dev/null +++ b/test_cases/q1/graph_bfs_vs_dfs.solution @@ -0,0 +1,7 @@ +# This is the solution file for test_cases/q1/graph_bfs_vs_dfs.test. +# This solution is designed to support both right-to-left +# and left-to-right implementations. +solution: "2:A->D 0:D->G" +expanded_states: "A D" +rev_solution: "0:A->B 0:B->D 0:D->G" +rev_expanded_states: "A B D" diff --git a/test_cases/q1/graph_bfs_vs_dfs.test b/test_cases/q1/graph_bfs_vs_dfs.test new file mode 100644 index 0000000..155e1fe --- /dev/null +++ b/test_cases/q1/graph_bfs_vs_dfs.test @@ -0,0 +1,30 @@ +# Graph where BFS finds the optimal solution but DFS does not +class: "GraphSearchTest" +algorithm: "depthFirstSearch" + +diagram: """ +/-- B +| ^ +| | +| *A -->[G] +| | ^ +| V | +\-->D ----/ + +A is the start state, G is the goal. Arrows +mark possible transitions +""" +# The following section specifies the search problem and the solution. +# The graph is specified by first the set of start states, followed by +# the set of goal states, and lastly by the state transitions which are +# of the form: +# +graph: """ +start_state: A +goal_states: G +A 0:A->B B 1.0 +A 1:A->G G 2.0 +A 2:A->D D 4.0 +B 0:B->D D 8.0 +D 0:D->G G 16.0 +""" diff --git a/test_cases/q1/graph_infinite.solution b/test_cases/q1/graph_infinite.solution new file mode 100644 index 0000000..82203ee --- /dev/null +++ b/test_cases/q1/graph_infinite.solution @@ -0,0 +1,7 @@ +# This is the solution file for test_cases/q1/graph_infinite.test. +# This solution is designed to support both right-to-left +# and left-to-right implementations. +solution: "0:A->B 1:B->C 1:C->G" +expanded_states: "A B C" +rev_solution: "0:A->B 1:B->C 1:C->G" +rev_expanded_states: "A B C" diff --git a/test_cases/q1/graph_infinite.test b/test_cases/q1/graph_infinite.test new file mode 100644 index 0000000..692ac05 --- /dev/null +++ b/test_cases/q1/graph_infinite.test @@ -0,0 +1,30 @@ +# Graph where natural action choice leads to an infinite loop +class: "GraphSearchTest" +algorithm: "depthFirstSearch" + +diagram: """ + B <--> C + ^ /| + | / | + V / V +*A<-/ [G] + +A is the start state, G is the goal. Arrows mark +possible state transitions. +""" +# The following section specifies the search problem and the solution. +# The graph is specified by first the set of start states, followed by +# the set of goal states, and lastly by the state transitions which are +# of the form: +# +graph: """ +start_state: A +goal_states: G +A 0:A->B B 1.0 +B 0:B->A A 2.0 +B 1:B->C C 4.0 +C 0:C->A A 8.0 +C 1:C->G G 16.0 +C 2:C->B B 32.0 +""" + diff --git a/test_cases/q1/graph_manypaths.solution b/test_cases/q1/graph_manypaths.solution new file mode 100644 index 0000000..34b5a82 --- /dev/null +++ b/test_cases/q1/graph_manypaths.solution @@ -0,0 +1,7 @@ +# This is the solution file for test_cases/q1/graph_manypaths.test. +# This solution is designed to support both right-to-left +# and left-to-right implementations. +solution: "2:A->B2 0:B2->C 0:C->D 2:D->E2 0:E2->F 0:F->G" +expanded_states: "A B2 C D E2 F" +rev_solution: "0:A->B1 0:B1->C 0:C->D 0:D->E1 0:E1->F 0:F->G" +rev_expanded_states: "A B1 C D E1 F" diff --git a/test_cases/q1/graph_manypaths.test b/test_cases/q1/graph_manypaths.test new file mode 100644 index 0000000..953c4eb --- /dev/null +++ b/test_cases/q1/graph_manypaths.test @@ -0,0 +1,39 @@ +class: "GraphSearchTest" +algorithm: "depthFirstSearch" + +diagram: """ + B1 E1 + ^ \ ^ \ + / V / V +*A --> C --> D --> F --> [G] + \ ^ \ ^ + V / V / + B2 E2 + +A is the start state, G is the goal. Arrows mark +possible state transitions. This graph has multiple +paths to the goal, where nodes with the same state +are added to the fringe multiple times before they +are expanded. +""" +# The following section specifies the search problem and the solution. +# The graph is specified by first the set of start states, followed by +# the set of goal states, and lastly by the state transitions which are +# of the form: +# +graph: """ +start_state: A +goal_states: G +A 0:A->B1 B1 1.0 +A 1:A->C C 2.0 +A 2:A->B2 B2 4.0 +B1 0:B1->C C 8.0 +B2 0:B2->C C 16.0 +C 0:C->D D 32.0 +D 0:D->E1 E1 64.0 +D 1:D->F F 128.0 +D 2:D->E2 E2 256.0 +E1 0:E1->F F 512.0 +E2 0:E2->F F 1024.0 +F 0:F->G G 2048.0 +""" diff --git a/test_cases/q1/pacman_1.solution b/test_cases/q1/pacman_1.solution new file mode 100644 index 0000000..82a670c --- /dev/null +++ b/test_cases/q1/pacman_1.solution @@ -0,0 +1,40 @@ +# This is the solution file for test_cases/q1/pacman_1.test. +# This solution is designed to support both right-to-left +# and left-to-right implementations. +# Number of nodes expanded must be with a factor of 1.0 of the numbers below. +solution: """ +West West West West West West West West West West West West West West +West West West West West West West West West West West West West West +West West West West West South South South South South South South +South South East East East North North North North North North North +East East South South South South South South East East North North +North North North North East East South South South South East East +North North East East East East East East East East South South South +East East East East East East East South South South South South South +South West West West West West West West West West West West West West +West West West West South West West West West West West West West West +""" +expanded_nodes: "146" +rev_solution: """ +South South West West West West South South East East East East South +South West West West West South South East East East East South South +West West West West South South South East North East East East South +South South West West West West West West West North North North North +North North North North West West West West West West West North North +North East East East East South East East East North North North West +West North North West West West West West West West West West West +West West West West West West West West West West West West West West +South South South South South South South South South East East East +North North North North North North North East East South South South +South South South East East North North North North North North East +East South South South South East East North North North North East +East East East East South South West West West South South East East +East South South West West West West West West South South West West +West West West South West West West West West South South East East +East East East East East North East East East East East North North +East East East East East East North East East East East East South +South West West West South West West West West West West South South +West West West West West South West West West West West West West West +West +""" +rev_expanded_nodes: "269" diff --git a/test_cases/q1/pacman_1.test b/test_cases/q1/pacman_1.test new file mode 100644 index 0000000..6ae5412 --- /dev/null +++ b/test_cases/q1/pacman_1.test @@ -0,0 +1,27 @@ +# This is a basic depth first search test +class: "PacmanSearchTest" +algorithm: "depthFirstSearch" + +# The following specifies the layout to be used +layoutName: "mediumMaze" +layout: """ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% P% +% %%%%%%%%%%%%%%%%%%%%%%% %%%%%%%% % +% %% % % %%%%%%% %% % +% %% % % % % %%%% %%%%%%%%% %% %%%%% +% %% % % % % %% %% % +% %% % % % % % %%%% %%% %%%%%% % +% % % % % % %% %%%%%%%% % +% %% % % %%%%%%%% %% %% %%%%% +% %% % %% %%%%%%%%% %% % +% %%%%%% %%%%%%% %% %%%%%% % +%%%%%% % %%%% %% % % +% %%%%%% %%%%% % %% %% %%%%% +% %%%%%% % %%%%% %% % +% %%%%%% %%%%%%%%%%% %% %% % +%%%%%%%%%% %%%%%% % +%. %%%%%%%%%%%%%%%% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +""" + diff --git a/test_cases/q2/CONFIG b/test_cases/q2/CONFIG new file mode 100644 index 0000000..ad7e38a --- /dev/null +++ b/test_cases/q2/CONFIG @@ -0,0 +1,2 @@ +max_points: "3" +class: "PassAllTestsQuestion" diff --git a/test_cases/q2/graph_backtrack.solution b/test_cases/q2/graph_backtrack.solution new file mode 100644 index 0000000..6c669c2 --- /dev/null +++ b/test_cases/q2/graph_backtrack.solution @@ -0,0 +1,7 @@ +# This is the solution file for test_cases/q2/graph_backtrack.test. +# This solution is designed to support both right-to-left +# and left-to-right implementations. +solution: "1:A->C 0:C->G" +expanded_states: "A B C D" +rev_solution: "1:A->C 0:C->G" +rev_expanded_states: "A D C B" diff --git a/test_cases/q2/graph_backtrack.test b/test_cases/q2/graph_backtrack.test new file mode 100644 index 0000000..2b35d8b --- /dev/null +++ b/test_cases/q2/graph_backtrack.test @@ -0,0 +1,32 @@ +class: "GraphSearchTest" +algorithm: "breadthFirstSearch" + +diagram: """ + B + ^ + | +*A --> C --> G + | + V + D + +A is the start state, G is the goal. Arrows mark +possible state transitions. This tests whether +you extract the sequence of actions correctly even +if your search backtracks. If you fail this, your +nodes are not correctly tracking the sequences of +actions required to reach them. +""" +# The following section specifies the search problem and the solution. +# The graph is specified by first the set of start states, followed by +# the set of goal states, and lastly by the state transitions which are +# of the form: +# +graph: """ +start_state: A +goal_states: G +A 0:A->B B 1.0 +A 1:A->C C 2.0 +A 2:A->D D 4.0 +C 0:C->G G 8.0 +""" diff --git a/test_cases/q2/graph_bfs_vs_dfs.solution b/test_cases/q2/graph_bfs_vs_dfs.solution new file mode 100644 index 0000000..05eecc8 --- /dev/null +++ b/test_cases/q2/graph_bfs_vs_dfs.solution @@ -0,0 +1,7 @@ +# This is the solution file for test_cases/q2/graph_bfs_vs_dfs.test. +# This solution is designed to support both right-to-left +# and left-to-right implementations. +solution: "1:A->G" +expanded_states: "A B" +rev_solution: "1:A->G" +rev_expanded_states: "A D" diff --git a/test_cases/q2/graph_bfs_vs_dfs.test b/test_cases/q2/graph_bfs_vs_dfs.test new file mode 100644 index 0000000..47b78a6 --- /dev/null +++ b/test_cases/q2/graph_bfs_vs_dfs.test @@ -0,0 +1,30 @@ +# Graph where BFS finds the optimal solution but DFS does not +class: "GraphSearchTest" +algorithm: "breadthFirstSearch" + +diagram: """ +/-- B +| ^ +| | +| *A -->[G] +| | ^ +| V | +\-->D ----/ + +A is the start state, G is the goal. Arrows +mark possible transitions +""" +# The following section specifies the search problem and the solution. +# The graph is specified by first the set of start states, followed by +# the set of goal states, and lastly by the state transitions which are +# of the form: +# +graph: """ +start_state: A +goal_states: G +A 0:A->B B 1.0 +A 1:A->G G 2.0 +A 2:A->D D 4.0 +B 0:B->D D 8.0 +D 0:D->G G 16.0 +""" diff --git a/test_cases/q2/graph_infinite.solution b/test_cases/q2/graph_infinite.solution new file mode 100644 index 0000000..17b621c --- /dev/null +++ b/test_cases/q2/graph_infinite.solution @@ -0,0 +1,7 @@ +# This is the solution file for test_cases/q2/graph_infinite.test. +# This solution is designed to support both right-to-left +# and left-to-right implementations. +solution: "0:A->B 1:B->C 1:C->G" +expanded_states: "A B C" +rev_solution: "0:A->B 1:B->C 1:C->G" +rev_expanded_states: "A B C" diff --git a/test_cases/q2/graph_infinite.test b/test_cases/q2/graph_infinite.test new file mode 100644 index 0000000..2cae9ad --- /dev/null +++ b/test_cases/q2/graph_infinite.test @@ -0,0 +1,30 @@ +# Graph where natural action choice leads to an infinite loop +class: "GraphSearchTest" +algorithm: "breadthFirstSearch" + +diagram: """ + B <--> C + ^ /| + | / | + V / V +*A<-/ [G] + +A is the start state, G is the goal. Arrows mark +possible state transitions. +""" +# The following section specifies the search problem and the solution. +# The graph is specified by first the set of start states, followed by +# the set of goal states, and lastly by the state transitions which are +# of the form: +# +graph: """ +start_state: A +goal_states: G +A 0:A->B B 1.0 +B 0:B->A A 2.0 +B 1:B->C C 4.0 +C 0:C->A A 8.0 +C 1:C->G G 16.0 +C 2:C->B B 32.0 +""" + diff --git a/test_cases/q2/graph_manypaths.solution b/test_cases/q2/graph_manypaths.solution new file mode 100644 index 0000000..0cea422 --- /dev/null +++ b/test_cases/q2/graph_manypaths.solution @@ -0,0 +1,7 @@ +# This is the solution file for test_cases/q2/graph_manypaths.test. +# This solution is designed to support both right-to-left +# and left-to-right implementations. +solution: "1:A->C 0:C->D 1:D->F 0:F->G" +expanded_states: "A B1 C B2 D E1 F E2" +rev_solution: "1:A->C 0:C->D 1:D->F 0:F->G" +rev_expanded_states: "A B2 C B1 D E2 F E1" diff --git a/test_cases/q2/graph_manypaths.test b/test_cases/q2/graph_manypaths.test new file mode 100644 index 0000000..7c636ea --- /dev/null +++ b/test_cases/q2/graph_manypaths.test @@ -0,0 +1,39 @@ +class: "GraphSearchTest" +algorithm: "breadthFirstSearch" + +diagram: """ + B1 E1 + ^ \ ^ \ + / V / V +*A --> C --> D --> F --> [G] + \ ^ \ ^ + V / V / + B2 E2 + +A is the start state, G is the goal. Arrows mark +possible state transitions. This graph has multiple +paths to the goal, where nodes with the same state +are added to the fringe multiple times before they +are expanded. +""" +# The following section specifies the search problem and the solution. +# The graph is specified by first the set of start states, followed by +# the set of goal states, and lastly by the state transitions which are +# of the form: +# +graph: """ +start_state: A +goal_states: G +A 0:A->B1 B1 1.0 +A 1:A->C C 2.0 +A 2:A->B2 B2 4.0 +B1 0:B1->C C 8.0 +B2 0:B2->C C 16.0 +C 0:C->D D 32.0 +D 0:D->E1 E1 64.0 +D 1:D->F F 128.0 +D 2:D->E2 E2 256.0 +E1 0:E1->F F 512.0 +E2 0:E2->F F 1024.0 +F 0:F->G G 2048.0 +""" diff --git a/test_cases/q2/pacman_1.solution b/test_cases/q2/pacman_1.solution new file mode 100644 index 0000000..8f6d2bd --- /dev/null +++ b/test_cases/q2/pacman_1.solution @@ -0,0 +1,22 @@ +# This is the solution file for test_cases/q2/pacman_1.test. +# This solution is designed to support both right-to-left +# and left-to-right implementations. +# Number of nodes expanded must be with a factor of 1.0 of the numbers below. +solution: """ +West West West West West West West West West South South East East +South South South West West West North West West West West South South +South East East East East East East East South South South South South +South South West West West West West West West West West West West +West West West West West West South West West West West West West West +West West +""" +expanded_nodes: "269" +rev_solution: """ +West West West West West West West West West South South East East +South South South West West West North West West West West South South +South East East East East East East East South South South South South +South South West West West West West West West West West West West +West West West West West West South West West West West West West West +West West +""" +rev_expanded_nodes: "269" diff --git a/test_cases/q2/pacman_1.test b/test_cases/q2/pacman_1.test new file mode 100644 index 0000000..c913f0c --- /dev/null +++ b/test_cases/q2/pacman_1.test @@ -0,0 +1,27 @@ +# This is a basic breadth first search test +class: "PacmanSearchTest" +algorithm: "breadthFirstSearch" + +# The following specifies the layout to be used +layoutName: "mediumMaze" +layout: """ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% P% +% %%%%%%%%%%%%%%%%%%%%%%% %%%%%%%% % +% %% % % %%%%%%% %% % +% %% % % % % %%%% %%%%%%%%% %% %%%%% +% %% % % % % %% %% % +% %% % % % % % %%%% %%% %%%%%% % +% % % % % % %% %%%%%%%% % +% %% % % %%%%%%%% %% %% %%%%% +% %% % %% %%%%%%%%% %% % +% %%%%%% %%%%%%% %% %%%%%% % +%%%%%% % %%%% %% % % +% %%%%%% %%%%% % %% %% %%%%% +% %%%%%% % %%%%% %% % +% %%%%%% %%%%%%%%%%% %% %% % +%%%%%%%%%% %%%%%% % +%. %%%%%%%%%%%%%%%% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +""" + diff --git a/test_cases/q3/CONFIG b/test_cases/q3/CONFIG new file mode 100644 index 0000000..e5332c3 --- /dev/null +++ b/test_cases/q3/CONFIG @@ -0,0 +1,2 @@ +class: "PassAllTestsQuestion" +max_points: "3" diff --git a/test_cases/q3/graph_backtrack.solution b/test_cases/q3/graph_backtrack.solution new file mode 100644 index 0000000..d150cb7 --- /dev/null +++ b/test_cases/q3/graph_backtrack.solution @@ -0,0 +1,7 @@ +# This is the solution file for test_cases/q3/graph_backtrack.test. +# This solution is designed to support both right-to-left +# and left-to-right implementations. +solution: "1:A->C 0:C->G" +expanded_states: "A B C D" +rev_solution: "1:A->C 0:C->G" +rev_expanded_states: "A B C D" diff --git a/test_cases/q3/graph_backtrack.test b/test_cases/q3/graph_backtrack.test new file mode 100644 index 0000000..a74bd9e --- /dev/null +++ b/test_cases/q3/graph_backtrack.test @@ -0,0 +1,32 @@ +class: "GraphSearchTest" +algorithm: "uniformCostSearch" + +diagram: """ + B + ^ + | +*A --> C --> G + | + V + D + +A is the start state, G is the goal. Arrows mark +possible state transitions. This tests whether +you extract the sequence of actions correctly even +if your search backtracks. If you fail this, your +nodes are not correctly tracking the sequences of +actions required to reach them. +""" +# The following section specifies the search problem and the solution. +# The graph is specified by first the set of start states, followed by +# the set of goal states, and lastly by the state transitions which are +# of the form: +# +graph: """ +start_state: A +goal_states: G +A 0:A->B B 1.0 +A 1:A->C C 2.0 +A 2:A->D D 4.0 +C 0:C->G G 8.0 +""" diff --git a/test_cases/q3/graph_bfs_vs_dfs.solution b/test_cases/q3/graph_bfs_vs_dfs.solution new file mode 100644 index 0000000..5dfffca --- /dev/null +++ b/test_cases/q3/graph_bfs_vs_dfs.solution @@ -0,0 +1,7 @@ +# This is the solution file for test_cases/q3/graph_bfs_vs_dfs.test. +# This solution is designed to support both right-to-left +# and left-to-right implementations. +solution: "1:A->G" +expanded_states: "A B" +rev_solution: "1:A->G" +rev_expanded_states: "A B" diff --git a/test_cases/q3/graph_bfs_vs_dfs.test b/test_cases/q3/graph_bfs_vs_dfs.test new file mode 100644 index 0000000..87aa465 --- /dev/null +++ b/test_cases/q3/graph_bfs_vs_dfs.test @@ -0,0 +1,30 @@ +# Graph where BFS finds the optimal solution but DFS does not +class: "GraphSearchTest" +algorithm: "uniformCostSearch" + +diagram: """ +/-- B +| ^ +| | +| *A -->[G] +| | ^ +| V | +\-->D ----/ + +A is the start state, G is the goal. Arrows +mark possible transitions +""" +# The following section specifies the search problem and the solution. +# The graph is specified by first the set of start states, followed by +# the set of goal states, and lastly by the state transitions which are +# of the form: +# +graph: """ +start_state: A +goal_states: G +A 0:A->B B 1.0 +A 1:A->G G 2.0 +A 2:A->D D 4.0 +B 0:B->D D 8.0 +D 0:D->G G 16.0 +""" diff --git a/test_cases/q3/graph_infinite.solution b/test_cases/q3/graph_infinite.solution new file mode 100644 index 0000000..c6cd195 --- /dev/null +++ b/test_cases/q3/graph_infinite.solution @@ -0,0 +1,7 @@ +# This is the solution file for test_cases/q3/graph_infinite.test. +# This solution is designed to support both right-to-left +# and left-to-right implementations. +solution: "0:A->B 1:B->C 1:C->G" +expanded_states: "A B C" +rev_solution: "0:A->B 1:B->C 1:C->G" +rev_expanded_states: "A B C" diff --git a/test_cases/q3/graph_infinite.test b/test_cases/q3/graph_infinite.test new file mode 100644 index 0000000..80d807f --- /dev/null +++ b/test_cases/q3/graph_infinite.test @@ -0,0 +1,30 @@ +# Graph where natural action choice leads to an infinite loop +class: "GraphSearchTest" +algorithm: "uniformCostSearch" + +diagram: """ + B <--> C + ^ /| + | / | + V / V +*A<-/ [G] + +A is the start state, G is the goal. Arrows mark +possible state transitions. +""" +# The following section specifies the search problem and the solution. +# The graph is specified by first the set of start states, followed by +# the set of goal states, and lastly by the state transitions which are +# of the form: +# +graph: """ +start_state: A +goal_states: G +A 0:A->B B 1.0 +B 0:B->A A 2.0 +B 1:B->C C 4.0 +C 0:C->A A 8.0 +C 1:C->G G 16.0 +C 2:C->B B 32.0 +""" + diff --git a/test_cases/q3/graph_manypaths.solution b/test_cases/q3/graph_manypaths.solution new file mode 100644 index 0000000..628568f --- /dev/null +++ b/test_cases/q3/graph_manypaths.solution @@ -0,0 +1,7 @@ +# This is the solution file for test_cases/q3/graph_manypaths.test. +# This solution is designed to support both right-to-left +# and left-to-right implementations. +solution: "1:A->C 0:C->D 1:D->F 0:F->G" +expanded_states: "A B1 C B2 D E1 F E2" +rev_solution: "1:A->C 0:C->D 1:D->F 0:F->G" +rev_expanded_states: "A B1 C B2 D E1 F E2" diff --git a/test_cases/q3/graph_manypaths.test b/test_cases/q3/graph_manypaths.test new file mode 100644 index 0000000..8c39dc7 --- /dev/null +++ b/test_cases/q3/graph_manypaths.test @@ -0,0 +1,39 @@ +class: "GraphSearchTest" +algorithm: "uniformCostSearch" + +diagram: """ + B1 E1 + ^ \ ^ \ + / V / V +*A --> C --> D --> F --> [G] + \ ^ \ ^ + V / V / + B2 E2 + +A is the start state, G is the goal. Arrows mark +possible state transitions. This graph has multiple +paths to the goal, where nodes with the same state +are added to the fringe multiple times before they +are expanded. +""" +# The following section specifies the search problem and the solution. +# The graph is specified by first the set of start states, followed by +# the set of goal states, and lastly by the state transitions which are +# of the form: +# +graph: """ +start_state: A +goal_states: G +A 0:A->B1 B1 1.0 +A 1:A->C C 2.0 +A 2:A->B2 B2 4.0 +B1 0:B1->C C 8.0 +B2 0:B2->C C 16.0 +C 0:C->D D 32.0 +D 0:D->E1 E1 64.0 +D 1:D->F F 128.0 +D 2:D->E2 E2 256.0 +E1 0:E1->F F 512.0 +E2 0:E2->F F 1024.0 +F 0:F->G G 2048.0 +""" diff --git a/test_cases/q3/ucs_0_graph.solution b/test_cases/q3/ucs_0_graph.solution new file mode 100644 index 0000000..b8c1509 --- /dev/null +++ b/test_cases/q3/ucs_0_graph.solution @@ -0,0 +1,7 @@ +# This is the solution file for test_cases/q3/ucs_0_graph.test. +# This solution is designed to support both right-to-left +# and left-to-right implementations. +solution: "Right Down Down" +expanded_states: "A B D C G" +rev_solution: "Right Down Down" +rev_expanded_states: "A B D C G" diff --git a/test_cases/q3/ucs_0_graph.test b/test_cases/q3/ucs_0_graph.test new file mode 100644 index 0000000..e8f3d4c --- /dev/null +++ b/test_cases/q3/ucs_0_graph.test @@ -0,0 +1,39 @@ +class: "GraphSearchTest" +algorithm: "uniformCostSearch" + +diagram: """ + C + ^ + | 2 + 2 V 4 +*A <----> B -----> [H] + |1 + 1.5 V 2.5 + G <----- D -----> E + | + 2 | + V + [F] + +A is the start state, F and H is the goal. Arrows mark possible state +transitions. The number next to the arrow is the cost of that transition. +""" +# The following section specifies the search problem and the solution. +# The graph is specified by first the set of start states, followed by +# the set of goal states, and lastly by the state transitions which are +# of the form: +# +graph: """ +start_state: A +goal_states: H F +A Right B 2.0 +B Right H 4.0 +B Down D 1.0 +B Up C 2.0 +B Left A 2.0 +C Down B 2.0 +D Right E 2.5 +D Down F 2.0 +D Left G 1.5 +""" + diff --git a/test_cases/q3/ucs_1_problemC.solution b/test_cases/q3/ucs_1_problemC.solution new file mode 100644 index 0000000..dc8fc4c --- /dev/null +++ b/test_cases/q3/ucs_1_problemC.solution @@ -0,0 +1,22 @@ +# This is the solution file for test_cases/q3/ucs_1_problemC.test. +# This solution is designed to support both right-to-left +# and left-to-right implementations. +# Number of nodes expanded must be with a factor of 1.1 of the numbers below. +solution: """ +West West West West West West West West West South South East East +South South South West West West North West West West West South South +South East East East East East East East South South South South South +South South West West West West West West West West West West West +West West West West West West South West West West West West West West +West West +""" +expanded_nodes: "269" +rev_solution: """ +West West West West West West West West West South South East East +South South South West West West North West West West West South South +South East East East East East East East South South South South South +South South West West West West West West West West West West West +West West West West West West South West West West West West West West +West West +""" +rev_expanded_nodes: "269" diff --git a/test_cases/q3/ucs_1_problemC.test b/test_cases/q3/ucs_1_problemC.test new file mode 100644 index 0000000..1ce714d --- /dev/null +++ b/test_cases/q3/ucs_1_problemC.test @@ -0,0 +1,28 @@ +class: "PacmanSearchTest" +algorithm: "uniformCostSearch" +points: "0.5" + +# The following specifies the layout to be used +layoutName: "mediumMaze" +layout: """ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% P% +% %%%%%%%%%%%%%%%%%%%%%%% %%%%%%%% % +% %% % % %%%%%%% %% % +% %% % % % % %%%% %%%%%%%%% %% %%%%% +% %% % % % % %% %% % +% %% % % % % % %%%% %%% %%%%%% % +% % % % % % %% %%%%%%%% % +% %% % % %%%%%%%% %% %% %%%%% +% %% % %% %%%%%%%%% %% % +% %%%%%% %%%%%%% %% %%%%%% % +%%%%%% % %%%% %% % % +% %%%%%% %%%%% % %% %% %%%%% +% %%%%%% % %%%%% %% % +% %%%%%% %%%%%%%%%%% %% %% % +%%%%%%%%%% %%%%%% % +%. %%%%%%%%%%%%%%%% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +""" +leewayFactor: "1.1" +#costFn: "lambda pos: 1" diff --git a/test_cases/q3/ucs_2_problemE.solution b/test_cases/q3/ucs_2_problemE.solution new file mode 100644 index 0000000..d84245f --- /dev/null +++ b/test_cases/q3/ucs_2_problemE.solution @@ -0,0 +1,22 @@ +# This is the solution file for test_cases/q3/ucs_2_problemE.test. +# This solution is designed to support both right-to-left +# and left-to-right implementations. +# Number of nodes expanded must be with a factor of 1.1 of the numbers below. +solution: """ +South South West West West West South South East East East East South +South West West West West South South East East East East South South +West West West West South South East East East East South South South +West West West West West West West North West West West West West West +West West West West West West West West West West West South West West +West West West West West West West +""" +expanded_nodes: "260" +rev_solution: """ +South South West West West West South South East East East East South +South West West West West South South East East East East South South +West West West West South South East East East East South South South +West West West West West West West North West West West West West West +West West West West West West West West West West West South West West +West West West West West West West +""" +rev_expanded_nodes: "260" diff --git a/test_cases/q3/ucs_2_problemE.test b/test_cases/q3/ucs_2_problemE.test new file mode 100644 index 0000000..3c609f2 --- /dev/null +++ b/test_cases/q3/ucs_2_problemE.test @@ -0,0 +1,28 @@ +class: "PacmanSearchTest" +algorithm: "uniformCostSearch" +points: "0.5" + +# The following specifies the layout to be used +layoutName: "mediumMaze" +layout: """ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% P% +% %%%%%%%%%%%%%%%%%%%%%%% %%%%%%%% % +% %% % % %%%%%%% %% % +% %% % % % % %%%% %%%%%%%%% %% %%%%% +% %% % % % % %% %% % +% %% % % % % % %%%% %%% %%%%%% % +% % % % % % %% %%%%%%%% % +% %% % % %%%%%%%% %% %% %%%%% +% %% % %% %%%%%%%%% %% % +% %%%%%% %%%%%%% %% %%%%%% % +%%%%%% % %%%% %% % % +% %%%%%% %%%%% % %% %% %%%%% +% %%%%%% % %%%%% %% % +% %%%%%% %%%%%%%%%%% %% %% % +%%%%%%%%%% %%%%%% % +%. %%%%%%%%%%%%%%%% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +""" +leewayFactor: "1.1" +costFn: "lambda pos: .5 ** pos[0]" diff --git a/test_cases/q3/ucs_3_problemW.solution b/test_cases/q3/ucs_3_problemW.solution new file mode 100644 index 0000000..e04325d --- /dev/null +++ b/test_cases/q3/ucs_3_problemW.solution @@ -0,0 +1,34 @@ +# This is the solution file for test_cases/q3/ucs_3_problemW.test. +# This solution is designed to support both right-to-left +# and left-to-right implementations. +# Number of nodes expanded must be with a factor of 1.1 of the numbers below. +solution: """ +West West West West West West West West West West West West West West +West West West West West West West West West West West West West West +West West West West West South South South South South South South +South South East East East North North North North North North North +East East South South South South South South East East North North +North North North North East East South South South South East East +North North East East South South East East East South South West West +West West West West South South West West West West West South West +West West West West South South East East East East East East East +North East East East East East North North East East East East East +East South South West West West West South South West West West West +West South West West West West West West West West West +""" +expanded_nodes: "173" +rev_solution: """ +West West West West West West West West West West West West West West +West West West West West West West West West West West West West West +West West West West West South South South South South South South +South South East East East North North North North North North North +East East South South South South South South East East North North +North North North North East East South South South South East East +North North East East South South East East East South South West West +West West West West South South West West West West West South West +West West West West South South East East East East East East East +North East East East East East North North East East East East East +East South South West West West West South South West West West West +West South West West West West West West West West West +""" +rev_expanded_nodes: "173" diff --git a/test_cases/q3/ucs_3_problemW.test b/test_cases/q3/ucs_3_problemW.test new file mode 100644 index 0000000..fbc2fad --- /dev/null +++ b/test_cases/q3/ucs_3_problemW.test @@ -0,0 +1,28 @@ +class: "PacmanSearchTest" +algorithm: "uniformCostSearch" +points: "0.5" + +# The following specifies the layout to be used +layoutName: "mediumMaze" +layout: """ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% P% +% %%%%%%%%%%%%%%%%%%%%%%% %%%%%%%% % +% %% % % %%%%%%% %% % +% %% % % % % %%%% %%%%%%%%% %% %%%%% +% %% % % % % %% %% % +% %% % % % % % %%%% %%% %%%%%% % +% % % % % % %% %%%%%%%% % +% %% % % %%%%%%%% %% %% %%%%% +% %% % %% %%%%%%%%% %% % +% %%%%%% %%%%%%% %% %%%%%% % +%%%%%% % %%%% %% % % +% %%%%%% %%%%% % %% %% %%%%% +% %%%%%% % %%%%% %% % +% %%%%%% %%%%%%%%%%% %% %% % +%%%%%%%%%% %%%%%% % +%. %%%%%%%%%%%%%%%% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +""" +leewayFactor: "1.1" +costFn: "lambda pos: 2 ** pos[0]" diff --git a/test_cases/q3/ucs_4_testSearch.solution b/test_cases/q3/ucs_4_testSearch.solution new file mode 100644 index 0000000..b8c5303 --- /dev/null +++ b/test_cases/q3/ucs_4_testSearch.solution @@ -0,0 +1,12 @@ +# This is the solution file for test_cases/q3/ucs_4_testSearch.test. +# This solution is designed to support both right-to-left +# and left-to-right implementations. +# Number of nodes expanded must be with a factor of 2.0 of the numbers below. +solution: """ +West East East South South West West +""" +expanded_nodes: "14" +rev_solution: """ +West East East South South West West +""" +rev_expanded_nodes: "13" diff --git a/test_cases/q3/ucs_4_testSearch.test b/test_cases/q3/ucs_4_testSearch.test new file mode 100644 index 0000000..a16c6d8 --- /dev/null +++ b/test_cases/q3/ucs_4_testSearch.test @@ -0,0 +1,16 @@ +class: "PacmanSearchTest" +algorithm: "uniformCostSearch" +points: "0.5" + +# The following specifies the layout to be used +layoutName: "testSearch" +layout: """ +%%%%% +%.P % +%%% % +%. % +%%%%% +""" +searchProblemClass: "FoodSearchProblem" +leewayFactor: "2" + diff --git a/test_cases/q3/ucs_5_goalAtDequeue.solution b/test_cases/q3/ucs_5_goalAtDequeue.solution new file mode 100644 index 0000000..7d6c982 --- /dev/null +++ b/test_cases/q3/ucs_5_goalAtDequeue.solution @@ -0,0 +1,7 @@ +# This is the solution file for test_cases/q3/ucs_5_goalAtDequeue.test. +# This solution is designed to support both right-to-left +# and left-to-right implementations. +solution: "1:A->B 0:B->C 0:C->G" +expanded_states: "A B C" +rev_solution: "1:A->B 0:B->C 0:C->G" +rev_expanded_states: "A B C" diff --git a/test_cases/q3/ucs_5_goalAtDequeue.test b/test_cases/q3/ucs_5_goalAtDequeue.test new file mode 100644 index 0000000..72b35bc --- /dev/null +++ b/test_cases/q3/ucs_5_goalAtDequeue.test @@ -0,0 +1,29 @@ +class: "GraphSearchTest" +algorithm: "uniformCostSearch" + +diagram: """ + 1 1 1 +*A ---> B ---> C ---> [G] + | ^ + | 10 | + \---------------------/ + +A is the start state, G is the goal. Arrows mark possible state +transitions. The number next to the arrow is the cost of that transition. + +If you fail this test case, you may be incorrectly testing if a node is a goal +before adding it into the queue, instead of testing when you remove the node +from the queue. See the algorithm pseudocode in lecture. +""" + +graph: """ +start_state: A +goal_states: G +A 0:A->G G 10.0 +A 1:A->B B 1.0 +B 0:B->C C 1.0 +C 0:C->G G 1.0 +""" +# We only care about the solution, not the expansion order. +exactExpansionOrder: "False" + diff --git a/test_cases/q4/CONFIG b/test_cases/q4/CONFIG new file mode 100644 index 0000000..b24223d --- /dev/null +++ b/test_cases/q4/CONFIG @@ -0,0 +1,2 @@ +class: "PassAllTestsQuestion" +max_points: "3" \ No newline at end of file diff --git a/test_cases/q4/astar_0.solution b/test_cases/q4/astar_0.solution new file mode 100644 index 0000000..459cadd --- /dev/null +++ b/test_cases/q4/astar_0.solution @@ -0,0 +1,7 @@ +# This is the solution file for test_cases/q4/astar_0.test. +# This solution is designed to support both right-to-left +# and left-to-right implementations. +solution: "Right Down Down" +expanded_states: "A B D C G" +rev_solution: "Right Down Down" +rev_expanded_states: "A B D C G" diff --git a/test_cases/q4/astar_0.test b/test_cases/q4/astar_0.test new file mode 100644 index 0000000..9b3b539 --- /dev/null +++ b/test_cases/q4/astar_0.test @@ -0,0 +1,39 @@ +class: "GraphSearchTest" +algorithm: "aStarSearch" + +diagram: """ + C + ^ + | 2 + 2 V 4 +*A <----> B -----> [H] + | + 1.5 V 2.5 + G <----- D -----> E + | + 2 | + V + [F] + +A is the start state, F and H is the goal. Arrows mark possible state +transitions. The number next to the arrow is the cost of that transition. +""" +# The following section specifies the search problem and the solution. +# The graph is specified by first the set of start states, followed by +# the set of goal states, and lastly by the state transitions which are +# of the form: +# +graph: """ +start_state: A +goal_states: H F +A Right B 2.0 +B Right H 4.0 +B Down D 1.0 +B Up C 2.0 +B Left A 2.0 +C Down B 2.0 +D Right E 2.5 +D Down F 2.0 +D Left G 1.5 +""" + diff --git a/test_cases/q4/astar_1_graph_heuristic.solution b/test_cases/q4/astar_1_graph_heuristic.solution new file mode 100644 index 0000000..7767c27 --- /dev/null +++ b/test_cases/q4/astar_1_graph_heuristic.solution @@ -0,0 +1,7 @@ +# This is the solution file for test_cases/q4/astar_1_graph_heuristic.test. +# This solution is designed to support both right-to-left +# and left-to-right implementations. +solution: "0 0 2" +expanded_states: "S A D C" +rev_solution: "0 0 2" +rev_expanded_states: "S A D C" diff --git a/test_cases/q4/astar_1_graph_heuristic.test b/test_cases/q4/astar_1_graph_heuristic.test new file mode 100644 index 0000000..b5afd79 --- /dev/null +++ b/test_cases/q4/astar_1_graph_heuristic.test @@ -0,0 +1,54 @@ +class: "GraphSearchTest" +algorithm: "aStarSearch" + +diagram: """ + 2 3 2 + S --- A --- C ---> G + | \ / ^ +3 | \ 5 / 1 / + | \ / / + B --- D -------/ + 4 5 + +S is the start state, G is the goal. Arrows mark possible state +transitions. The number next to the arrow is the cost of that transition. + +The heuristic value of each state is: + S 6.0 + A 2.5 + B 5.25 + C 1.125 + D 1.0625 + G 0 +""" +# The following section specifies the search problem and the solution. +# The graph is specified by first the set of start states, followed by +# the set of goal states, and lastly by the state transitions which are +# of the form: +# +graph: """ +start_state: S +goal_states: G +S 0 A 2.0 +S 1 B 3.0 +S 2 D 5.0 +A 0 C 3.0 +A 1 S 2.0 +B 0 D 4.0 +B 1 S 3.0 +C 0 A 3.0 +C 1 D 1.0 +C 2 G 2.0 +D 0 B 4.0 +D 1 C 1.0 +D 2 G 5.0 +D 3 S 5.0 +""" +heuristic: """ +S 6.0 +A 2.5 +B 5.25 +C 1.125 +D 1.0625 +G 0 +""" diff --git a/test_cases/q4/astar_2_manhattan.solution b/test_cases/q4/astar_2_manhattan.solution new file mode 100644 index 0000000..65bf5f5 --- /dev/null +++ b/test_cases/q4/astar_2_manhattan.solution @@ -0,0 +1,22 @@ +# This is the solution file for test_cases/q4/astar_2_manhattan.test. +# This solution is designed to support both right-to-left +# and left-to-right implementations. +# Number of nodes expanded must be with a factor of 1.1 of the numbers below. +solution: """ +West West West West West West West West West South South East East +South South South West West West North West West West West South South +South East East East East East East East South South South South South +South South West West West West West West West West West West West +West West West West West West South West West West West West West West +West West +""" +expanded_nodes: "221" +rev_solution: """ +West West West West West West West West West South South East East +South South South West West West North West West West West South South +South East East East East East East East South South South South South +South South West West West West West West West West West West West +West West West West West West South West West West West West West West +West West +""" +rev_expanded_nodes: "221" diff --git a/test_cases/q4/astar_2_manhattan.test b/test_cases/q4/astar_2_manhattan.test new file mode 100644 index 0000000..e936195 --- /dev/null +++ b/test_cases/q4/astar_2_manhattan.test @@ -0,0 +1,27 @@ +class: "PacmanSearchTest" +algorithm: "aStarSearch" + +# The following specifies the layout to be used +layoutName: "mediumMaze" +layout: """ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% P% +% %%%%%%%%%%%%%%%%%%%%%%% %%%%%%%% % +% %% % % %%%%%%% %% % +% %% % % % % %%%% %%%%%%%%% %% %%%%% +% %% % % % % %% %% % +% %% % % % % % %%%% %%% %%%%%% % +% % % % % % %% %%%%%%%% % +% %% % % %%%%%%%% %% %% %%%%% +% %% % %% %%%%%%%%% %% % +% %%%%%% %%%%%%% %% %%%%%% % +%%%%%% % %%%% %% % % +% %%%%%% %%%%% % %% %% %%%%% +% %%%%%% % %%%%% %% % +% %%%%%% %%%%%%%%%%% %% %% % +%%%%%%%%%% %%%%%% % +%. %%%%%%%%%%%%%%%% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +""" +leewayFactor: "1.1" +heuristic: "manhattanHeuristic" diff --git a/test_cases/q4/astar_3_goalAtDequeue.solution b/test_cases/q4/astar_3_goalAtDequeue.solution new file mode 100644 index 0000000..edb3502 --- /dev/null +++ b/test_cases/q4/astar_3_goalAtDequeue.solution @@ -0,0 +1,7 @@ +# This is the solution file for test_cases/q4/astar_3_goalAtDequeue.test. +# This solution is designed to support both right-to-left +# and left-to-right implementations. +solution: "1:A->B 0:B->C 0:C->G" +expanded_states: "A B C" +rev_solution: "1:A->B 0:B->C 0:C->G" +rev_expanded_states: "A B C" diff --git a/test_cases/q4/astar_3_goalAtDequeue.test b/test_cases/q4/astar_3_goalAtDequeue.test new file mode 100644 index 0000000..c4d1903 --- /dev/null +++ b/test_cases/q4/astar_3_goalAtDequeue.test @@ -0,0 +1,29 @@ +class: "GraphSearchTest" +algorithm: "aStarSearch" + +diagram: """ + 1 1 1 +*A ---> B ---> C ---> [G] + | ^ + | 10 | + \---------------------/ + +A is the start state, G is the goal. Arrows mark possible state +transitions. The number next to the arrow is the cost of that transition. + +If you fail this test case, you may be incorrectly testing if a node is a goal +before adding it into the queue, instead of testing when you remove the node +from the queue. See the algorithm pseudocode in lecture. +""" + +graph: """ +start_state: A +goal_states: G +A 0:A->G G 10.0 +A 1:A->B B 1.0 +B 0:B->C C 1.0 +C 0:C->G G 1.0 +""" +# We only care about the solution, not the expansion order. +exactExpansionOrder: "False" + diff --git a/test_cases/q4/graph_backtrack.solution b/test_cases/q4/graph_backtrack.solution new file mode 100644 index 0000000..fc51794 --- /dev/null +++ b/test_cases/q4/graph_backtrack.solution @@ -0,0 +1,7 @@ +# This is the solution file for test_cases/q4/graph_backtrack.test. +# This solution is designed to support both right-to-left +# and left-to-right implementations. +solution: "1:A->C 0:C->G" +expanded_states: "A B C D" +rev_solution: "1:A->C 0:C->G" +rev_expanded_states: "A B C D" diff --git a/test_cases/q4/graph_backtrack.test b/test_cases/q4/graph_backtrack.test new file mode 100644 index 0000000..84e0126 --- /dev/null +++ b/test_cases/q4/graph_backtrack.test @@ -0,0 +1,32 @@ +class: "GraphSearchTest" +algorithm: "aStarSearch" + +diagram: """ + B + ^ + | +*A --> C --> G + | + V + D + +A is the start state, G is the goal. Arrows mark +possible state transitions. This tests whether +you extract the sequence of actions correctly even +if your search backtracks. If you fail this, your +nodes are not correctly tracking the sequences of +actions required to reach them. +""" +# The following section specifies the search problem and the solution. +# The graph is specified by first the set of start states, followed by +# the set of goal states, and lastly by the state transitions which are +# of the form: +# +graph: """ +start_state: A +goal_states: G +A 0:A->B B 1.0 +A 1:A->C C 2.0 +A 2:A->D D 4.0 +C 0:C->G G 8.0 +""" diff --git a/test_cases/q4/graph_manypaths.solution b/test_cases/q4/graph_manypaths.solution new file mode 100644 index 0000000..0caa767 --- /dev/null +++ b/test_cases/q4/graph_manypaths.solution @@ -0,0 +1,7 @@ +# This is the solution file for test_cases/q4/graph_manypaths.test. +# This solution is designed to support both right-to-left +# and left-to-right implementations. +solution: "1:A->C 0:C->D 1:D->F 0:F->G" +expanded_states: "A B1 C B2 D E1 F E2" +rev_solution: "1:A->C 0:C->D 1:D->F 0:F->G" +rev_expanded_states: "A B1 C B2 D E1 F E2" diff --git a/test_cases/q4/graph_manypaths.test b/test_cases/q4/graph_manypaths.test new file mode 100644 index 0000000..82fdf87 --- /dev/null +++ b/test_cases/q4/graph_manypaths.test @@ -0,0 +1,39 @@ +class: "GraphSearchTest" +algorithm: "aStarSearch" + +diagram: """ + B1 E1 + ^ \ ^ \ + / V / V +*A --> C --> D --> F --> [G] + \ ^ \ ^ + V / V / + B2 E2 + +A is the start state, G is the goal. Arrows mark +possible state transitions. This graph has multiple +paths to the goal, where nodes with the same state +are added to the fringe multiple times before they +are expanded. +""" +# The following section specifies the search problem and the solution. +# The graph is specified by first the set of start states, followed by +# the set of goal states, and lastly by the state transitions which are +# of the form: +# +graph: """ +start_state: A +goal_states: G +A 0:A->B1 B1 1.0 +A 1:A->C C 2.0 +A 2:A->B2 B2 4.0 +B1 0:B1->C C 8.0 +B2 0:B2->C C 16.0 +C 0:C->D D 32.0 +D 0:D->E1 E1 64.0 +D 1:D->F F 128.0 +D 2:D->E2 E2 256.0 +E1 0:E1->F F 512.0 +E2 0:E2->F F 1024.0 +F 0:F->G G 2048.0 +""" diff --git a/test_cases/q5/CONFIG b/test_cases/q5/CONFIG new file mode 100644 index 0000000..e7c6582 --- /dev/null +++ b/test_cases/q5/CONFIG @@ -0,0 +1,3 @@ +class: "PassAllTestsQuestion" +max_points: "3" +depends: "q2" \ No newline at end of file diff --git a/test_cases/q5/corner_tiny_corner.solution b/test_cases/q5/corner_tiny_corner.solution new file mode 100644 index 0000000..161bf15 --- /dev/null +++ b/test_cases/q5/corner_tiny_corner.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q5/corner_tiny_corner.test. +solution_length: "28" diff --git a/test_cases/q5/corner_tiny_corner.test b/test_cases/q5/corner_tiny_corner.test new file mode 100644 index 0000000..823bd47 --- /dev/null +++ b/test_cases/q5/corner_tiny_corner.test @@ -0,0 +1,14 @@ +class: "CornerProblemTest" + +layoutName: "tinyCorner" +layout: """ +%%%%%%%% +%. .% +% P % +% %%%% % +% % % +% % %%%% +%.% .% +%%%%%%%% +""" + diff --git a/test_cases/q6/CONFIG b/test_cases/q6/CONFIG new file mode 100644 index 0000000..b76e0eb --- /dev/null +++ b/test_cases/q6/CONFIG @@ -0,0 +1,3 @@ +class: "Q6PartialCreditQuestion" +max_points: "3" +depends: "q4" \ No newline at end of file diff --git a/test_cases/q6/corner_sanity_1.solution b/test_cases/q6/corner_sanity_1.solution new file mode 100644 index 0000000..4385d9b --- /dev/null +++ b/test_cases/q6/corner_sanity_1.solution @@ -0,0 +1,7 @@ +# In order for a heuristic to be admissible, the value +# of the heuristic must be less at each state than the +# true cost of the optimal path from that state to a goal. +cost: "8" +path: """ +North South South East East East North North +""" diff --git a/test_cases/q6/corner_sanity_1.test b/test_cases/q6/corner_sanity_1.test new file mode 100644 index 0000000..93379ac --- /dev/null +++ b/test_cases/q6/corner_sanity_1.test @@ -0,0 +1,12 @@ +class: "CornerHeuristicSanity" +points: "1" + +# The following specifies the layout to be used +layout: """ +%%%%%% +%. .% +%P % +%. .% +%%%%%% +""" + diff --git a/test_cases/q6/corner_sanity_2.solution b/test_cases/q6/corner_sanity_2.solution new file mode 100644 index 0000000..1aebe8a --- /dev/null +++ b/test_cases/q6/corner_sanity_2.solution @@ -0,0 +1,7 @@ +# In order for a heuristic to be admissible, the value +# of the heuristic must be less at each state than the +# true cost of the optimal path from that state to a goal. +cost: "8" +path: """ +West North North East East East South South +""" diff --git a/test_cases/q6/corner_sanity_2.test b/test_cases/q6/corner_sanity_2.test new file mode 100644 index 0000000..18184a8 --- /dev/null +++ b/test_cases/q6/corner_sanity_2.test @@ -0,0 +1,12 @@ +class: "CornerHeuristicSanity" +points: "1" + +# The following specifies the layout to be used +layout: """ +%%%%%% +%. .% +% %% % +%.P%.% +%%%%%% +""" + diff --git a/test_cases/q6/corner_sanity_3.solution b/test_cases/q6/corner_sanity_3.solution new file mode 100644 index 0000000..c02dd57 --- /dev/null +++ b/test_cases/q6/corner_sanity_3.solution @@ -0,0 +1,9 @@ +# In order for a heuristic to be admissible, the value +# of the heuristic must be less at each state than the +# true cost of the optimal path from that state to a goal. +cost: "28" +path: """ +South South South West West West West East East East East East North +North North North North West West West South South South West West +North North North +""" diff --git a/test_cases/q6/corner_sanity_3.test b/test_cases/q6/corner_sanity_3.test new file mode 100644 index 0000000..8f30442 --- /dev/null +++ b/test_cases/q6/corner_sanity_3.test @@ -0,0 +1,15 @@ +class: "CornerHeuristicSanity" +points: "1" + +# The following specifies the layout to be used +layout: """ +%%%%%%%% +%.% .% +% % % % +% % %P % +% % % +%%%%% % +%. .% +%%%%%%%% +""" + diff --git a/test_cases/q6/medium_corners.solution b/test_cases/q6/medium_corners.solution new file mode 100644 index 0000000..913dc45 --- /dev/null +++ b/test_cases/q6/medium_corners.solution @@ -0,0 +1,16 @@ +# This solution file specifies the length of the optimal path +# as well as the thresholds on number of nodes expanded to be +# used in scoring. +cost: "106" +path: """ +North East East East East North North West West West West North North +North North North North North North West West West West South South +East East East East South South South South South South West West +South South South West West East East North North North East East East +East East East East East South South East East East East East North +North East East North North East East North North East East East East +South South South South East East North North East East South South +South South South North North North North North North North West West +North North East East North North +""" +thresholds: "2000 1600 1200" diff --git a/test_cases/q6/medium_corners.test b/test_cases/q6/medium_corners.test new file mode 100644 index 0000000..dfa0a68 --- /dev/null +++ b/test_cases/q6/medium_corners.test @@ -0,0 +1,19 @@ +class: "CornerHeuristicPacman" + +# The following specifies the layout to be used +layout: """ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%. % % % %.% +% % % %%%%%% %%%%%%% % % +% % % % % % +%%%%% %%%%% %%% %% %%%%% % %%% +% % % % % % % % % +% %%% % % % %%%%%%%% %%% %%% % +% % %% % % % % +%%% % %%%%%%% %%%% %%% % % % % +% % %% % % % +% % %%%%% % %%%% % %%% %%% % % +% % % % % % %%% % +%. %P%%%%% % %%% % .% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +""" diff --git a/test_cases/q7/CONFIG b/test_cases/q7/CONFIG new file mode 100644 index 0000000..ee85183 --- /dev/null +++ b/test_cases/q7/CONFIG @@ -0,0 +1,3 @@ +class: "PartialCreditQuestion" +max_points: "4" +depends: "q4" \ No newline at end of file diff --git a/test_cases/q7/food_heuristic_1.solution b/test_cases/q7/food_heuristic_1.solution new file mode 100644 index 0000000..7a287f8 --- /dev/null +++ b/test_cases/q7/food_heuristic_1.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q7/food_heuristic_1.test. +solution_cost: "0" diff --git a/test_cases/q7/food_heuristic_1.test b/test_cases/q7/food_heuristic_1.test new file mode 100644 index 0000000..7545a7a --- /dev/null +++ b/test_cases/q7/food_heuristic_1.test @@ -0,0 +1,13 @@ +class: "HeuristicTest" + +heuristic: "foodHeuristic" +searchProblemClass: "FoodSearchProblem" +layoutName: "Test 1" +layout: """ +%%%%%% +% % +% % +%P % +%%%%%% +""" + diff --git a/test_cases/q7/food_heuristic_10.solution b/test_cases/q7/food_heuristic_10.solution new file mode 100644 index 0000000..1917f05 --- /dev/null +++ b/test_cases/q7/food_heuristic_10.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q7/food_heuristic_10.test. +solution_cost: "7" diff --git a/test_cases/q7/food_heuristic_10.test b/test_cases/q7/food_heuristic_10.test new file mode 100644 index 0000000..212c7bd --- /dev/null +++ b/test_cases/q7/food_heuristic_10.test @@ -0,0 +1,13 @@ +class: "HeuristicTest" + +heuristic: "foodHeuristic" +searchProblemClass: "FoodSearchProblem" +layoutName: "Test 10" +layout: """ +%%%%%%%% +% % +%. P .% +% % +%%%%%%%% +""" + diff --git a/test_cases/q7/food_heuristic_11.solution b/test_cases/q7/food_heuristic_11.solution new file mode 100644 index 0000000..11c3289 --- /dev/null +++ b/test_cases/q7/food_heuristic_11.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q7/food_heuristic_11.test. +solution_cost: "8" diff --git a/test_cases/q7/food_heuristic_11.test b/test_cases/q7/food_heuristic_11.test new file mode 100644 index 0000000..f5e6ed4 --- /dev/null +++ b/test_cases/q7/food_heuristic_11.test @@ -0,0 +1,13 @@ +class: "HeuristicTest" + +heuristic: "foodHeuristic" +searchProblemClass: "FoodSearchProblem" +layoutName: "Test 11" +layout: """ +%%%%%%%% +% % +% P % +%. . .% +%%%%%%%% +""" + diff --git a/test_cases/q7/food_heuristic_12.solution b/test_cases/q7/food_heuristic_12.solution new file mode 100644 index 0000000..0edcc02 --- /dev/null +++ b/test_cases/q7/food_heuristic_12.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q7/food_heuristic_12.test. +solution_cost: "1" diff --git a/test_cases/q7/food_heuristic_12.test b/test_cases/q7/food_heuristic_12.test new file mode 100644 index 0000000..cc99a25 --- /dev/null +++ b/test_cases/q7/food_heuristic_12.test @@ -0,0 +1,13 @@ +class: "HeuristicTest" + +heuristic: "foodHeuristic" +searchProblemClass: "FoodSearchProblem" +layoutName: "Test 12" +layout: """ +%%%%%%%% +% % +% P.% +% % +%%%%%%%% +""" + diff --git a/test_cases/q7/food_heuristic_13.solution b/test_cases/q7/food_heuristic_13.solution new file mode 100644 index 0000000..c25d50b --- /dev/null +++ b/test_cases/q7/food_heuristic_13.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q7/food_heuristic_13.test. +solution_cost: "5" diff --git a/test_cases/q7/food_heuristic_13.test b/test_cases/q7/food_heuristic_13.test new file mode 100644 index 0000000..09d6f1e --- /dev/null +++ b/test_cases/q7/food_heuristic_13.test @@ -0,0 +1,13 @@ +class: "HeuristicTest" + +heuristic: "foodHeuristic" +searchProblemClass: "FoodSearchProblem" +layoutName: "Test 13" +layout: """ +%%%%%%%% +% % +%P. .% +% % +%%%%%%%% +""" + diff --git a/test_cases/q7/food_heuristic_14.solution b/test_cases/q7/food_heuristic_14.solution new file mode 100644 index 0000000..e6cc475 --- /dev/null +++ b/test_cases/q7/food_heuristic_14.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q7/food_heuristic_14.test. +solution_cost: "31" diff --git a/test_cases/q7/food_heuristic_14.test b/test_cases/q7/food_heuristic_14.test new file mode 100644 index 0000000..58982e3 --- /dev/null +++ b/test_cases/q7/food_heuristic_14.test @@ -0,0 +1,19 @@ +class: "HeuristicTest" + +heuristic: "foodHeuristic" +searchProblemClass: "FoodSearchProblem" +layoutName: "Test 14" +layout: """ +%%%%%%%%%% +% % +% ...%...% +% .%.%.%.% +% .%.%.%.% +% .%.%.%.% +% .%.%.%.% +% .%.%.%.% +%P.%...%.% +% % +%%%%%%%%%% +""" + diff --git a/test_cases/q7/food_heuristic_15.solution b/test_cases/q7/food_heuristic_15.solution new file mode 100644 index 0000000..4eca0f1 --- /dev/null +++ b/test_cases/q7/food_heuristic_15.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q7/food_heuristic_15.test. +solution_cost: "21" diff --git a/test_cases/q7/food_heuristic_15.test b/test_cases/q7/food_heuristic_15.test new file mode 100644 index 0000000..df605c1 --- /dev/null +++ b/test_cases/q7/food_heuristic_15.test @@ -0,0 +1,32 @@ +class: "HeuristicTest" + +heuristic: "foodHeuristic" +searchProblemClass: "FoodSearchProblem" +layoutName: "Test 15" +layout: """ +%%% +% % +% % +% % +% % +% % +%.% +%.% +% % +% % +% % +% % +% % +% % +% % +%.% +% % +%P% +% % +% % +% % +% % +%.% +%%% +""" + diff --git a/test_cases/q7/food_heuristic_16.solution b/test_cases/q7/food_heuristic_16.solution new file mode 100644 index 0000000..8d89992 --- /dev/null +++ b/test_cases/q7/food_heuristic_16.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q7/food_heuristic_16.test. +solution_cost: "7" diff --git a/test_cases/q7/food_heuristic_16.test b/test_cases/q7/food_heuristic_16.test new file mode 100644 index 0000000..762b433 --- /dev/null +++ b/test_cases/q7/food_heuristic_16.test @@ -0,0 +1,15 @@ +class: "HeuristicTest" + +heuristic: "foodHeuristic" +searchProblemClass: "FoodSearchProblem" +layoutName: "Test 16" +layout: """ +%%%% +% .% +% % +%P % +% % +% .% +%%%% +""" + diff --git a/test_cases/q7/food_heuristic_17.solution b/test_cases/q7/food_heuristic_17.solution new file mode 100644 index 0000000..63a9a1b --- /dev/null +++ b/test_cases/q7/food_heuristic_17.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q7/food_heuristic_17.test. +solution_cost: "16" diff --git a/test_cases/q7/food_heuristic_17.test b/test_cases/q7/food_heuristic_17.test new file mode 100644 index 0000000..a923f67 --- /dev/null +++ b/test_cases/q7/food_heuristic_17.test @@ -0,0 +1,14 @@ +class: "HeuristicTest" + +heuristic: "foodHeuristic" +searchProblemClass: "FoodSearchProblem" +layoutName: "Test 17" +layout: """ +%%%%%%%% +%.%....% +%.% %%.% +%.%P%%.% +%... .% +%%%%%%%% +""" + diff --git a/test_cases/q7/food_heuristic_2.solution b/test_cases/q7/food_heuristic_2.solution new file mode 100644 index 0000000..ca5aba1 --- /dev/null +++ b/test_cases/q7/food_heuristic_2.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q7/food_heuristic_2.test. +solution_cost: "0" diff --git a/test_cases/q7/food_heuristic_2.test b/test_cases/q7/food_heuristic_2.test new file mode 100644 index 0000000..956e75d --- /dev/null +++ b/test_cases/q7/food_heuristic_2.test @@ -0,0 +1,32 @@ +class: "HeuristicTest" + +heuristic: "foodHeuristic" +searchProblemClass: "FoodSearchProblem" +layoutName: "Test 2" +layout: """ +%%% +% % +% % +% % +% % +% % +% % +% % +% % +% % +% % +% % +% % +% % +% % +% % +% % +%P% +% % +% % +% % +% % +% % +%%% +""" + diff --git a/test_cases/q7/food_heuristic_3.solution b/test_cases/q7/food_heuristic_3.solution new file mode 100644 index 0000000..d1694b5 --- /dev/null +++ b/test_cases/q7/food_heuristic_3.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q7/food_heuristic_3.test. +solution_cost: "0" diff --git a/test_cases/q7/food_heuristic_3.test b/test_cases/q7/food_heuristic_3.test new file mode 100644 index 0000000..250a8b1 --- /dev/null +++ b/test_cases/q7/food_heuristic_3.test @@ -0,0 +1,15 @@ +class: "HeuristicTest" + +heuristic: "foodHeuristic" +searchProblemClass: "FoodSearchProblem" +layoutName: "Test 3" +layout: """ +%%%% +% % +% % +%P % +% % +% % +%%%% +""" + diff --git a/test_cases/q7/food_heuristic_4.solution b/test_cases/q7/food_heuristic_4.solution new file mode 100644 index 0000000..6e1e82a --- /dev/null +++ b/test_cases/q7/food_heuristic_4.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q7/food_heuristic_4.test. +solution_cost: "0" diff --git a/test_cases/q7/food_heuristic_4.test b/test_cases/q7/food_heuristic_4.test new file mode 100644 index 0000000..ed86a0c --- /dev/null +++ b/test_cases/q7/food_heuristic_4.test @@ -0,0 +1,14 @@ +class: "HeuristicTest" + +heuristic: "foodHeuristic" +searchProblemClass: "FoodSearchProblem" +layoutName: "Test 4" +layout: """ +%%%%%%%% +% % % +% % %% % +% %P%% % +% % +%%%%%%%% +""" + diff --git a/test_cases/q7/food_heuristic_5.solution b/test_cases/q7/food_heuristic_5.solution new file mode 100644 index 0000000..779e9e6 --- /dev/null +++ b/test_cases/q7/food_heuristic_5.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q7/food_heuristic_5.test. +solution_cost: "11" diff --git a/test_cases/q7/food_heuristic_5.test b/test_cases/q7/food_heuristic_5.test new file mode 100644 index 0000000..1f44c48 --- /dev/null +++ b/test_cases/q7/food_heuristic_5.test @@ -0,0 +1,13 @@ +class: "HeuristicTest" + +heuristic: "foodHeuristic" +searchProblemClass: "FoodSearchProblem" +layoutName: "Test 5" +layout: """ +%%%%%% +%....% +%....% +%P...% +%%%%%% +""" + diff --git a/test_cases/q7/food_heuristic_6.solution b/test_cases/q7/food_heuristic_6.solution new file mode 100644 index 0000000..906b510 --- /dev/null +++ b/test_cases/q7/food_heuristic_6.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q7/food_heuristic_6.test. +solution_cost: "5" diff --git a/test_cases/q7/food_heuristic_6.test b/test_cases/q7/food_heuristic_6.test new file mode 100644 index 0000000..01d7f32 --- /dev/null +++ b/test_cases/q7/food_heuristic_6.test @@ -0,0 +1,13 @@ +class: "HeuristicTest" + +heuristic: "foodHeuristic" +searchProblemClass: "FoodSearchProblem" +layoutName: "Test 6" +layout: """ +%%%%%% +% .% +%.P..% +% % +%%%%%% +""" + diff --git a/test_cases/q7/food_heuristic_7.solution b/test_cases/q7/food_heuristic_7.solution new file mode 100644 index 0000000..5994a7b --- /dev/null +++ b/test_cases/q7/food_heuristic_7.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q7/food_heuristic_7.test. +solution_cost: "7" diff --git a/test_cases/q7/food_heuristic_7.test b/test_cases/q7/food_heuristic_7.test new file mode 100644 index 0000000..b1db372 --- /dev/null +++ b/test_cases/q7/food_heuristic_7.test @@ -0,0 +1,13 @@ +class: "HeuristicTest" + +heuristic: "foodHeuristic" +searchProblemClass: "FoodSearchProblem" +layoutName: "Test 7" +layout: """ +%%%%%%% +% .% +%. P..% +% % +%%%%%%% +""" + diff --git a/test_cases/q7/food_heuristic_8.solution b/test_cases/q7/food_heuristic_8.solution new file mode 100644 index 0000000..0e4fb08 --- /dev/null +++ b/test_cases/q7/food_heuristic_8.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q7/food_heuristic_8.test. +solution_cost: "5" diff --git a/test_cases/q7/food_heuristic_8.test b/test_cases/q7/food_heuristic_8.test new file mode 100644 index 0000000..b9430af --- /dev/null +++ b/test_cases/q7/food_heuristic_8.test @@ -0,0 +1,13 @@ +class: "HeuristicTest" + +heuristic: "foodHeuristic" +searchProblemClass: "FoodSearchProblem" +layoutName: "Test 8" +layout: """ +%%%%%% +% .% +% .% +%P .% +%%%%%% +""" + diff --git a/test_cases/q7/food_heuristic_9.solution b/test_cases/q7/food_heuristic_9.solution new file mode 100644 index 0000000..1470d9a --- /dev/null +++ b/test_cases/q7/food_heuristic_9.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q7/food_heuristic_9.test. +solution_cost: "6" diff --git a/test_cases/q7/food_heuristic_9.test b/test_cases/q7/food_heuristic_9.test new file mode 100644 index 0000000..799b41d --- /dev/null +++ b/test_cases/q7/food_heuristic_9.test @@ -0,0 +1,13 @@ +class: "HeuristicTest" + +heuristic: "foodHeuristic" +searchProblemClass: "FoodSearchProblem" +layoutName: "Test 9" +layout: """ +%%%%%% +% %. % +% %%.% +%P. .% +%%%%%% +""" + diff --git a/test_cases/q7/food_heuristic_grade_tricky.solution b/test_cases/q7/food_heuristic_grade_tricky.solution new file mode 100644 index 0000000..cd6fd7d --- /dev/null +++ b/test_cases/q7/food_heuristic_grade_tricky.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q7/food_heuristic_grade_tricky.test. +# File intentionally blank. diff --git a/test_cases/q7/food_heuristic_grade_tricky.test b/test_cases/q7/food_heuristic_grade_tricky.test new file mode 100644 index 0000000..081fb0d --- /dev/null +++ b/test_cases/q7/food_heuristic_grade_tricky.test @@ -0,0 +1,19 @@ +class: "HeuristicGrade" + +heuristic: "foodHeuristic" +searchProblemClass: "FoodSearchProblem" +layoutName: "trickySearch" +layout: """ +%%%%%%%%%%%%%%%%%%%% +%. ..% % +%.%%.%%.%%.%%.%% % % +% P % % +%%%%%%%%%%%%%%%%%% % +%..... % +%%%%%%%%%%%%%%%%%%%% +""" +# One point always, an extra point for each +# threshold passed. +basePoints: "1" +gradingThresholds: "15000 12000 9000 7000" + diff --git a/test_cases/q8/CONFIG b/test_cases/q8/CONFIG new file mode 100644 index 0000000..b24223d --- /dev/null +++ b/test_cases/q8/CONFIG @@ -0,0 +1,2 @@ +class: "PassAllTestsQuestion" +max_points: "3" \ No newline at end of file diff --git a/test_cases/q8/closest_dot_1.solution b/test_cases/q8/closest_dot_1.solution new file mode 100644 index 0000000..300fc25 --- /dev/null +++ b/test_cases/q8/closest_dot_1.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q8/closest_dot_1.test. +solution_length: "1" diff --git a/test_cases/q8/closest_dot_1.test b/test_cases/q8/closest_dot_1.test new file mode 100644 index 0000000..672989f --- /dev/null +++ b/test_cases/q8/closest_dot_1.test @@ -0,0 +1,11 @@ +class: "ClosestDotTest" + +layoutName: "Test 1" +layout: """ +%%%%%% +%....% +%....% +%P...% +%%%%%% +""" + diff --git a/test_cases/q8/closest_dot_10.solution b/test_cases/q8/closest_dot_10.solution new file mode 100644 index 0000000..174b5dd --- /dev/null +++ b/test_cases/q8/closest_dot_10.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q8/closest_dot_10.test. +solution_length: "1" diff --git a/test_cases/q8/closest_dot_10.test b/test_cases/q8/closest_dot_10.test new file mode 100644 index 0000000..b1e0f33 --- /dev/null +++ b/test_cases/q8/closest_dot_10.test @@ -0,0 +1,17 @@ +class: "ClosestDotTest" + +layoutName: "Test 10" +layout: """ +%%%%%%%%%% +% % +% ...%...% +% .%.%.%.% +% .%.%.%.% +% .%.%.%.% +% .%.%.%.% +% .%.%.%.% +%P.%...%.% +% % +%%%%%%%%%% +""" + diff --git a/test_cases/q8/closest_dot_11.solution b/test_cases/q8/closest_dot_11.solution new file mode 100644 index 0000000..80bbe38 --- /dev/null +++ b/test_cases/q8/closest_dot_11.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q8/closest_dot_11.test. +solution_length: "2" diff --git a/test_cases/q8/closest_dot_11.test b/test_cases/q8/closest_dot_11.test new file mode 100644 index 0000000..0310a1e --- /dev/null +++ b/test_cases/q8/closest_dot_11.test @@ -0,0 +1,30 @@ +class: "ClosestDotTest" + +layoutName: "Test 11" +layout: """ +%%% +% % +% % +% % +% % +% % +%.% +%.% +% % +% % +% % +% % +% % +% % +% % +%.% +% % +%P% +% % +% % +% % +% % +%.% +%%% +""" + diff --git a/test_cases/q8/closest_dot_12.solution b/test_cases/q8/closest_dot_12.solution new file mode 100644 index 0000000..6f38bcb --- /dev/null +++ b/test_cases/q8/closest_dot_12.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q8/closest_dot_12.test. +solution_length: "3" diff --git a/test_cases/q8/closest_dot_12.test b/test_cases/q8/closest_dot_12.test new file mode 100644 index 0000000..a17b628 --- /dev/null +++ b/test_cases/q8/closest_dot_12.test @@ -0,0 +1,13 @@ +class: "ClosestDotTest" + +layoutName: "Test 12" +layout: """ +%%%% +% .% +% % +%P % +% % +% .% +%%%% +""" + diff --git a/test_cases/q8/closest_dot_13.solution b/test_cases/q8/closest_dot_13.solution new file mode 100644 index 0000000..7afa908 --- /dev/null +++ b/test_cases/q8/closest_dot_13.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q8/closest_dot_13.test. +solution_length: "1" diff --git a/test_cases/q8/closest_dot_13.test b/test_cases/q8/closest_dot_13.test new file mode 100644 index 0000000..87c423d --- /dev/null +++ b/test_cases/q8/closest_dot_13.test @@ -0,0 +1,12 @@ +class: "ClosestDotTest" + +layoutName: "Test 13" +layout: """ +%%%%%%%% +%.%....% +%.% %%.% +%.%P%%.% +%... .% +%%%%%%%% +""" + diff --git a/test_cases/q8/closest_dot_2.solution b/test_cases/q8/closest_dot_2.solution new file mode 100644 index 0000000..16d75de --- /dev/null +++ b/test_cases/q8/closest_dot_2.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q8/closest_dot_2.test. +solution_length: "1" diff --git a/test_cases/q8/closest_dot_2.test b/test_cases/q8/closest_dot_2.test new file mode 100644 index 0000000..4b59602 --- /dev/null +++ b/test_cases/q8/closest_dot_2.test @@ -0,0 +1,11 @@ +class: "ClosestDotTest" + +layoutName: "Test 2" +layout: """ +%%%%%% +% .% +%.P..% +% % +%%%%%% +""" + diff --git a/test_cases/q8/closest_dot_3.solution b/test_cases/q8/closest_dot_3.solution new file mode 100644 index 0000000..cbd5974 --- /dev/null +++ b/test_cases/q8/closest_dot_3.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q8/closest_dot_3.test. +solution_length: "1" diff --git a/test_cases/q8/closest_dot_3.test b/test_cases/q8/closest_dot_3.test new file mode 100644 index 0000000..aa2a3af --- /dev/null +++ b/test_cases/q8/closest_dot_3.test @@ -0,0 +1,11 @@ +class: "ClosestDotTest" + +layoutName: "Test 3" +layout: """ +%%%%%%% +% .% +%. P..% +% % +%%%%%%% +""" + diff --git a/test_cases/q8/closest_dot_4.solution b/test_cases/q8/closest_dot_4.solution new file mode 100644 index 0000000..ca520b5 --- /dev/null +++ b/test_cases/q8/closest_dot_4.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q8/closest_dot_4.test. +solution_length: "3" diff --git a/test_cases/q8/closest_dot_4.test b/test_cases/q8/closest_dot_4.test new file mode 100644 index 0000000..8499f6d --- /dev/null +++ b/test_cases/q8/closest_dot_4.test @@ -0,0 +1,11 @@ +class: "ClosestDotTest" + +layoutName: "Test 4" +layout: """ +%%%%%% +% .% +% .% +%P .% +%%%%%% +""" + diff --git a/test_cases/q8/closest_dot_5.solution b/test_cases/q8/closest_dot_5.solution new file mode 100644 index 0000000..5c526a2 --- /dev/null +++ b/test_cases/q8/closest_dot_5.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q8/closest_dot_5.test. +solution_length: "1" diff --git a/test_cases/q8/closest_dot_5.test b/test_cases/q8/closest_dot_5.test new file mode 100644 index 0000000..dfaee3d --- /dev/null +++ b/test_cases/q8/closest_dot_5.test @@ -0,0 +1,11 @@ +class: "ClosestDotTest" + +layoutName: "Test 5" +layout: """ +%%%%%% +% %. % +% %%.% +%P. .% +%%%%%% +""" + diff --git a/test_cases/q8/closest_dot_6.solution b/test_cases/q8/closest_dot_6.solution new file mode 100644 index 0000000..b06468a --- /dev/null +++ b/test_cases/q8/closest_dot_6.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q8/closest_dot_6.test. +solution_length: "2" diff --git a/test_cases/q8/closest_dot_6.test b/test_cases/q8/closest_dot_6.test new file mode 100644 index 0000000..bc50c57 --- /dev/null +++ b/test_cases/q8/closest_dot_6.test @@ -0,0 +1,11 @@ +class: "ClosestDotTest" + +layoutName: "Test 6" +layout: """ +%%%%%%%% +% % +%. P .% +% % +%%%%%%%% +""" + diff --git a/test_cases/q8/closest_dot_7.solution b/test_cases/q8/closest_dot_7.solution new file mode 100644 index 0000000..3231b28 --- /dev/null +++ b/test_cases/q8/closest_dot_7.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q8/closest_dot_7.test. +solution_length: "1" diff --git a/test_cases/q8/closest_dot_7.test b/test_cases/q8/closest_dot_7.test new file mode 100644 index 0000000..746e89a --- /dev/null +++ b/test_cases/q8/closest_dot_7.test @@ -0,0 +1,11 @@ +class: "ClosestDotTest" + +layoutName: "Test 7" +layout: """ +%%%%%%%% +% % +% P % +%. . .% +%%%%%%%% +""" + diff --git a/test_cases/q8/closest_dot_8.solution b/test_cases/q8/closest_dot_8.solution new file mode 100644 index 0000000..646e621 --- /dev/null +++ b/test_cases/q8/closest_dot_8.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q8/closest_dot_8.test. +solution_length: "1" diff --git a/test_cases/q8/closest_dot_8.test b/test_cases/q8/closest_dot_8.test new file mode 100644 index 0000000..c266ae1 --- /dev/null +++ b/test_cases/q8/closest_dot_8.test @@ -0,0 +1,11 @@ +class: "ClosestDotTest" + +layoutName: "Test 8" +layout: """ +%%%%%%%% +% % +% P.% +% % +%%%%%%%% +""" + diff --git a/test_cases/q8/closest_dot_9.solution b/test_cases/q8/closest_dot_9.solution new file mode 100644 index 0000000..6c94aa5 --- /dev/null +++ b/test_cases/q8/closest_dot_9.solution @@ -0,0 +1,2 @@ +# This is the solution file for test_cases/q8/closest_dot_9.test. +solution_length: "1" diff --git a/test_cases/q8/closest_dot_9.test b/test_cases/q8/closest_dot_9.test new file mode 100644 index 0000000..da078de --- /dev/null +++ b/test_cases/q8/closest_dot_9.test @@ -0,0 +1,11 @@ +class: "ClosestDotTest" + +layoutName: "Test 9" +layout: """ +%%%%%%%% +% % +%P. .% +% % +%%%%%%%% +""" + diff --git a/textDisplay.py b/textDisplay.py new file mode 100644 index 0000000..e920ad4 --- /dev/null +++ b/textDisplay.py @@ -0,0 +1,81 @@ +# textDisplay.py +# -------------- +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). + + +import time +try: + import pacman +except: + pass + +DRAW_EVERY = 1 +SLEEP_TIME = 0 # This can be overwritten by __init__ +DISPLAY_MOVES = False +QUIET = False # Supresses output + +class NullGraphics: + def initialize(self, state, isBlue = False): + pass + + def update(self, state): + pass + + def checkNullDisplay(self): + return True + + def pause(self): + time.sleep(SLEEP_TIME) + + def draw(self, state): + print state + + def updateDistributions(self, dist): + pass + + def finish(self): + pass + +class PacmanGraphics: + def __init__(self, speed=None): + if speed != None: + global SLEEP_TIME + SLEEP_TIME = speed + + def initialize(self, state, isBlue = False): + self.draw(state) + self.pause() + self.turn = 0 + self.agentCounter = 0 + + def update(self, state): + numAgents = len(state.agentStates) + self.agentCounter = (self.agentCounter + 1) % numAgents + if self.agentCounter == 0: + self.turn += 1 + if DISPLAY_MOVES: + ghosts = [pacman.nearestPoint(state.getGhostPosition(i)) for i in range(1, numAgents)] + print "%4d) P: %-8s" % (self.turn, str(pacman.nearestPoint(state.getPacmanPosition()))),'| Score: %-5d' % state.score,'| Ghosts:', ghosts + if self.turn % DRAW_EVERY == 0: + self.draw(state) + self.pause() + if state._win or state._lose: + self.draw(state) + + def pause(self): + time.sleep(SLEEP_TIME) + + def draw(self, state): + print state + + def finish(self): + pass diff --git a/util.py b/util.py new file mode 100644 index 0000000..5b066ed --- /dev/null +++ b/util.py @@ -0,0 +1,674 @@ +# util.py +# ------- +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). + + +# util.py +# ------- +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). + + +import sys +import inspect +import heapq, random +import cStringIO + + +class FixedRandom: + def __init__(self): + fixedState = (3, (2147483648L, 507801126L, 683453281L, 310439348L, 2597246090L, \ + 2209084787L, 2267831527L, 979920060L, 3098657677L, 37650879L, 807947081L, 3974896263L, \ + 881243242L, 3100634921L, 1334775171L, 3965168385L, 746264660L, 4074750168L, 500078808L, \ + 776561771L, 702988163L, 1636311725L, 2559226045L, 157578202L, 2498342920L, 2794591496L, \ + 4130598723L, 496985844L, 2944563015L, 3731321600L, 3514814613L, 3362575829L, 3038768745L, \ + 2206497038L, 1108748846L, 1317460727L, 3134077628L, 988312410L, 1674063516L, 746456451L, \ + 3958482413L, 1857117812L, 708750586L, 1583423339L, 3466495450L, 1536929345L, 1137240525L, \ + 3875025632L, 2466137587L, 1235845595L, 4214575620L, 3792516855L, 657994358L, 1241843248L, \ + 1695651859L, 3678946666L, 1929922113L, 2351044952L, 2317810202L, 2039319015L, 460787996L, \ + 3654096216L, 4068721415L, 1814163703L, 2904112444L, 1386111013L, 574629867L, 2654529343L, \ + 3833135042L, 2725328455L, 552431551L, 4006991378L, 1331562057L, 3710134542L, 303171486L, \ + 1203231078L, 2670768975L, 54570816L, 2679609001L, 578983064L, 1271454725L, 3230871056L, \ + 2496832891L, 2944938195L, 1608828728L, 367886575L, 2544708204L, 103775539L, 1912402393L, \ + 1098482180L, 2738577070L, 3091646463L, 1505274463L, 2079416566L, 659100352L, 839995305L, \ + 1696257633L, 274389836L, 3973303017L, 671127655L, 1061109122L, 517486945L, 1379749962L, \ + 3421383928L, 3116950429L, 2165882425L, 2346928266L, 2892678711L, 2936066049L, 1316407868L, \ + 2873411858L, 4279682888L, 2744351923L, 3290373816L, 1014377279L, 955200944L, 4220990860L, \ + 2386098930L, 1772997650L, 3757346974L, 1621616438L, 2877097197L, 442116595L, 2010480266L, \ + 2867861469L, 2955352695L, 605335967L, 2222936009L, 2067554933L, 4129906358L, 1519608541L, \ + 1195006590L, 1942991038L, 2736562236L, 279162408L, 1415982909L, 4099901426L, 1732201505L, \ + 2934657937L, 860563237L, 2479235483L, 3081651097L, 2244720867L, 3112631622L, 1636991639L, \ + 3860393305L, 2312061927L, 48780114L, 1149090394L, 2643246550L, 1764050647L, 3836789087L, \ + 3474859076L, 4237194338L, 1735191073L, 2150369208L, 92164394L, 756974036L, 2314453957L, \ + 323969533L, 4267621035L, 283649842L, 810004843L, 727855536L, 1757827251L, 3334960421L, \ + 3261035106L, 38417393L, 2660980472L, 1256633965L, 2184045390L, 811213141L, 2857482069L, \ + 2237770878L, 3891003138L, 2787806886L, 2435192790L, 2249324662L, 3507764896L, 995388363L, \ + 856944153L, 619213904L, 3233967826L, 3703465555L, 3286531781L, 3863193356L, 2992340714L, \ + 413696855L, 3865185632L, 1704163171L, 3043634452L, 2225424707L, 2199018022L, 3506117517L, \ + 3311559776L, 3374443561L, 1207829628L, 668793165L, 1822020716L, 2082656160L, 1160606415L, \ + 3034757648L, 741703672L, 3094328738L, 459332691L, 2702383376L, 1610239915L, 4162939394L, \ + 557861574L, 3805706338L, 3832520705L, 1248934879L, 3250424034L, 892335058L, 74323433L, \ + 3209751608L, 3213220797L, 3444035873L, 3743886725L, 1783837251L, 610968664L, 580745246L, \ + 4041979504L, 201684874L, 2673219253L, 1377283008L, 3497299167L, 2344209394L, 2304982920L, \ + 3081403782L, 2599256854L, 3184475235L, 3373055826L, 695186388L, 2423332338L, 222864327L, \ + 1258227992L, 3627871647L, 3487724980L, 4027953808L, 3053320360L, 533627073L, 3026232514L, \ + 2340271949L, 867277230L, 868513116L, 2158535651L, 2487822909L, 3428235761L, 3067196046L, \ + 3435119657L, 1908441839L, 788668797L, 3367703138L, 3317763187L, 908264443L, 2252100381L, \ + 764223334L, 4127108988L, 384641349L, 3377374722L, 1263833251L, 1958694944L, 3847832657L, \ + 1253909612L, 1096494446L, 555725445L, 2277045895L, 3340096504L, 1383318686L, 4234428127L, \ + 1072582179L, 94169494L, 1064509968L, 2681151917L, 2681864920L, 734708852L, 1338914021L, \ + 1270409500L, 1789469116L, 4191988204L, 1716329784L, 2213764829L, 3712538840L, 919910444L, \ + 1318414447L, 3383806712L, 3054941722L, 3378649942L, 1205735655L, 1268136494L, 2214009444L, \ + 2532395133L, 3232230447L, 230294038L, 342599089L, 772808141L, 4096882234L, 3146662953L, \ + 2784264306L, 1860954704L, 2675279609L, 2984212876L, 2466966981L, 2627986059L, 2985545332L, \ + 2578042598L, 1458940786L, 2944243755L, 3959506256L, 1509151382L, 325761900L, 942251521L, \ + 4184289782L, 2756231555L, 3297811774L, 1169708099L, 3280524138L, 3805245319L, 3227360276L, \ + 3199632491L, 2235795585L, 2865407118L, 36763651L, 2441503575L, 3314890374L, 1755526087L, \ + 17915536L, 1196948233L, 949343045L, 3815841867L, 489007833L, 2654997597L, 2834744136L, \ + 417688687L, 2843220846L, 85621843L, 747339336L, 2043645709L, 3520444394L, 1825470818L, \ + 647778910L, 275904777L, 1249389189L, 3640887431L, 4200779599L, 323384601L, 3446088641L, \ + 4049835786L, 1718989062L, 3563787136L, 44099190L, 3281263107L, 22910812L, 1826109246L, \ + 745118154L, 3392171319L, 1571490704L, 354891067L, 815955642L, 1453450421L, 940015623L, \ + 796817754L, 1260148619L, 3898237757L, 176670141L, 1870249326L, 3317738680L, 448918002L, \ + 4059166594L, 2003827551L, 987091377L, 224855998L, 3520570137L, 789522610L, 2604445123L, \ + 454472869L, 475688926L, 2990723466L, 523362238L, 3897608102L, 806637149L, 2642229586L, \ + 2928614432L, 1564415411L, 1691381054L, 3816907227L, 4082581003L, 1895544448L, 3728217394L, \ + 3214813157L, 4054301607L, 1882632454L, 2873728645L, 3694943071L, 1297991732L, 2101682438L, \ + 3952579552L, 678650400L, 1391722293L, 478833748L, 2976468591L, 158586606L, 2576499787L, \ + 662690848L, 3799889765L, 3328894692L, 2474578497L, 2383901391L, 1718193504L, 3003184595L, \ + 3630561213L, 1929441113L, 3848238627L, 1594310094L, 3040359840L, 3051803867L, 2462788790L, \ + 954409915L, 802581771L, 681703307L, 545982392L, 2738993819L, 8025358L, 2827719383L, \ + 770471093L, 3484895980L, 3111306320L, 3900000891L, 2116916652L, 397746721L, 2087689510L, \ + 721433935L, 1396088885L, 2751612384L, 1998988613L, 2135074843L, 2521131298L, 707009172L, \ + 2398321482L, 688041159L, 2264560137L, 482388305L, 207864885L, 3735036991L, 3490348331L, \ + 1963642811L, 3260224305L, 3493564223L, 1939428454L, 1128799656L, 1366012432L, 2858822447L, \ + 1428147157L, 2261125391L, 1611208390L, 1134826333L, 2374102525L, 3833625209L, 2266397263L, \ + 3189115077L, 770080230L, 2674657172L, 4280146640L, 3604531615L, 4235071805L, 3436987249L, \ + 509704467L, 2582695198L, 4256268040L, 3391197562L, 1460642842L, 1617931012L, 457825497L, \ + 1031452907L, 1330422862L, 4125947620L, 2280712485L, 431892090L, 2387410588L, 2061126784L, \ + 896457479L, 3480499461L, 2488196663L, 4021103792L, 1877063114L, 2744470201L, 1046140599L, \ + 2129952955L, 3583049218L, 4217723693L, 2720341743L, 820661843L, 1079873609L, 3360954200L, \ + 3652304997L, 3335838575L, 2178810636L, 1908053374L, 4026721976L, 1793145418L, 476541615L, \ + 973420250L, 515553040L, 919292001L, 2601786155L, 1685119450L, 3030170809L, 1590676150L, \ + 1665099167L, 651151584L, 2077190587L, 957892642L, 646336572L, 2743719258L, 866169074L, \ + 851118829L, 4225766285L, 963748226L, 799549420L, 1955032629L, 799460000L, 2425744063L, \ + 2441291571L, 1928963772L, 528930629L, 2591962884L, 3495142819L, 1896021824L, 901320159L, \ + 3181820243L, 843061941L, 3338628510L, 3782438992L, 9515330L, 1705797226L, 953535929L, \ + 764833876L, 3202464965L, 2970244591L, 519154982L, 3390617541L, 566616744L, 3438031503L, \ + 1853838297L, 170608755L, 1393728434L, 676900116L, 3184965776L, 1843100290L, 78995357L, \ + 2227939888L, 3460264600L, 1745705055L, 1474086965L, 572796246L, 4081303004L, 882828851L, \ + 1295445825L, 137639900L, 3304579600L, 2722437017L, 4093422709L, 273203373L, 2666507854L, \ + 3998836510L, 493829981L, 1623949669L, 3482036755L, 3390023939L, 833233937L, 1639668730L, \ + 1499455075L, 249728260L, 1210694006L, 3836497489L, 1551488720L, 3253074267L, 3388238003L, \ + 2372035079L, 3945715164L, 2029501215L, 3362012634L, 2007375355L, 4074709820L, 631485888L, \ + 3135015769L, 4273087084L, 3648076204L, 2739943601L, 1374020358L, 1760722448L, 3773939706L, \ + 1313027823L, 1895251226L, 4224465911L, 421382535L, 1141067370L, 3660034846L, 3393185650L, \ + 1850995280L, 1451917312L, 3841455409L, 3926840308L, 1397397252L, 2572864479L, 2500171350L, \ + 3119920613L, 531400869L, 1626487579L, 1099320497L, 407414753L, 2438623324L, 99073255L, \ + 3175491512L, 656431560L, 1153671785L, 236307875L, 2824738046L, 2320621382L, 892174056L, \ + 230984053L, 719791226L, 2718891946L, 624L), None) + self.random = random.Random() + self.random.setstate(fixedState) + +""" + Data structures useful for implementing SearchAgents +""" + +class Stack: + "A container with a last-in-first-out (LIFO) queuing policy." + def __init__(self): + self.list = [] + + def push(self,item): + "Push 'item' onto the stack" + self.list.append(item) + + def pop(self): + "Pop the most recently pushed item from the stack" + return self.list.pop() + + def isEmpty(self): + "Returns true if the stack is empty" + return len(self.list) == 0 + +class Queue: + "A container with a first-in-first-out (FIFO) queuing policy." + def __init__(self): + self.list = [] + + def push(self,item): + "Enqueue the 'item' into the queue" + self.list.insert(0,item) + + def pop(self): + """ + Dequeue the earliest enqueued item still in the queue. This + operation removes the item from the queue. + """ + return self.list.pop() + + def isEmpty(self): + "Returns true if the queue is empty" + return len(self.list) == 0 + +class PriorityQueue: + """ + Implements a priority queue data structure. Each inserted item + has a priority associated with it and the client is usually interested + in quick retrieval of the lowest-priority item in the queue. This + data structure allows O(1) access to the lowest-priority item. + """ + def __init__(self): + self.heap = [] + self.count = 0 + + def push(self, item, priority): + entry = (priority, self.count, item) + heapq.heappush(self.heap, entry) + self.count += 1 + + def pop(self): + (_, _, item) = heapq.heappop(self.heap) + return item + + def isEmpty(self): + return len(self.heap) == 0 + + def update(self, item, priority): + # If item already in priority queue with higher priority, update its priority and rebuild the heap. + # If item already in priority queue with equal or lower priority, do nothing. + # If item not in priority queue, do the same thing as self.push. + for index, (p, c, i) in enumerate(self.heap): + if i == item: + if p <= priority: + break + del self.heap[index] + self.heap.append((priority, c, item)) + heapq.heapify(self.heap) + break + else: + self.push(item, priority) + +class PriorityQueueWithFunction(PriorityQueue): + """ + Implements a priority queue with the same push/pop signature of the + Queue and the Stack classes. This is designed for drop-in replacement for + those two classes. The caller has to provide a priority function, which + extracts each item's priority. + """ + def __init__(self, priorityFunction): + "priorityFunction (item) -> priority" + self.priorityFunction = priorityFunction # store the priority function + PriorityQueue.__init__(self) # super-class initializer + + def push(self, item): + "Adds an item to the queue with priority from the priority function" + PriorityQueue.push(self, item, self.priorityFunction(item)) + + +def manhattanDistance( xy1, xy2 ): + "Returns the Manhattan distance between points xy1 and xy2" + return abs( xy1[0] - xy2[0] ) + abs( xy1[1] - xy2[1] ) + +""" + Data structures and functions useful for various course projects + + The search project should not need anything below this line. +""" + +class Counter(dict): + """ + A counter keeps track of counts for a set of keys. + + The counter class is an extension of the standard python + dictionary type. It is specialized to have number values + (integers or floats), and includes a handful of additional + functions to ease the task of counting data. In particular, + all keys are defaulted to have value 0. Using a dictionary: + + a = {} + print a['test'] + + would give an error, while the Counter class analogue: + + >>> a = Counter() + >>> print a['test'] + 0 + + returns the default 0 value. Note that to reference a key + that you know is contained in the counter, + you can still use the dictionary syntax: + + >>> a = Counter() + >>> a['test'] = 2 + >>> print a['test'] + 2 + + This is very useful for counting things without initializing their counts, + see for example: + + >>> a['blah'] += 1 + >>> print a['blah'] + 1 + + The counter also includes additional functionality useful in implementing + the classifiers for this assignment. Two counters can be added, + subtracted or multiplied together. See below for details. They can + also be normalized and their total count and arg max can be extracted. + """ + def __getitem__(self, idx): + self.setdefault(idx, 0) + return dict.__getitem__(self, idx) + + def incrementAll(self, keys, count): + """ + Increments all elements of keys by the same count. + + >>> a = Counter() + >>> a.incrementAll(['one','two', 'three'], 1) + >>> a['one'] + 1 + >>> a['two'] + 1 + """ + for key in keys: + self[key] += count + + def argMax(self): + """ + Returns the key with the highest value. + """ + if len(self.keys()) == 0: return None + all = self.items() + values = [x[1] for x in all] + maxIndex = values.index(max(values)) + return all[maxIndex][0] + + def sortedKeys(self): + """ + Returns a list of keys sorted by their values. Keys + with the highest values will appear first. + + >>> a = Counter() + >>> a['first'] = -2 + >>> a['second'] = 4 + >>> a['third'] = 1 + >>> a.sortedKeys() + ['second', 'third', 'first'] + """ + sortedItems = self.items() + compare = lambda x, y: sign(y[1] - x[1]) + sortedItems.sort(cmp=compare) + return [x[0] for x in sortedItems] + + def totalCount(self): + """ + Returns the sum of counts for all keys. + """ + return sum(self.values()) + + def normalize(self): + """ + Edits the counter such that the total count of all + keys sums to 1. The ratio of counts for all keys + will remain the same. Note that normalizing an empty + Counter will result in an error. + """ + total = float(self.totalCount()) + if total == 0: return + for key in self.keys(): + self[key] = self[key] / total + + def divideAll(self, divisor): + """ + Divides all counts by divisor + """ + divisor = float(divisor) + for key in self: + self[key] /= divisor + + def copy(self): + """ + Returns a copy of the counter + """ + return Counter(dict.copy(self)) + + def __mul__(self, y ): + """ + Multiplying two counters gives the dot product of their vectors where + each unique label is a vector element. + + >>> a = Counter() + >>> b = Counter() + >>> a['first'] = -2 + >>> a['second'] = 4 + >>> b['first'] = 3 + >>> b['second'] = 5 + >>> a['third'] = 1.5 + >>> a['fourth'] = 2.5 + >>> a * b + 14 + """ + sum = 0 + x = self + if len(x) > len(y): + x,y = y,x + for key in x: + if key not in y: + continue + sum += x[key] * y[key] + return sum + + def __radd__(self, y): + """ + Adding another counter to a counter increments the current counter + by the values stored in the second counter. + + >>> a = Counter() + >>> b = Counter() + >>> a['first'] = -2 + >>> a['second'] = 4 + >>> b['first'] = 3 + >>> b['third'] = 1 + >>> a += b + >>> a['first'] + 1 + """ + for key, value in y.items(): + self[key] += value + + def __add__( self, y ): + """ + Adding two counters gives a counter with the union of all keys and + counts of the second added to counts of the first. + + >>> a = Counter() + >>> b = Counter() + >>> a['first'] = -2 + >>> a['second'] = 4 + >>> b['first'] = 3 + >>> b['third'] = 1 + >>> (a + b)['first'] + 1 + """ + addend = Counter() + for key in self: + if key in y: + addend[key] = self[key] + y[key] + else: + addend[key] = self[key] + for key in y: + if key in self: + continue + addend[key] = y[key] + return addend + + def __sub__( self, y ): + """ + Subtracting a counter from another gives a counter with the union of all keys and + counts of the second subtracted from counts of the first. + + >>> a = Counter() + >>> b = Counter() + >>> a['first'] = -2 + >>> a['second'] = 4 + >>> b['first'] = 3 + >>> b['third'] = 1 + >>> (a - b)['first'] + -5 + """ + addend = Counter() + for key in self: + if key in y: + addend[key] = self[key] - y[key] + else: + addend[key] = self[key] + for key in y: + if key in self: + continue + addend[key] = -1 * y[key] + return addend + +def raiseNotDefined(): + fileName = inspect.stack()[1][1] + line = inspect.stack()[1][2] + method = inspect.stack()[1][3] + + print "*** Method not implemented: %s at line %s of %s" % (method, line, fileName) + sys.exit(1) + +def normalize(vectorOrCounter): + """ + normalize a vector or counter by dividing each value by the sum of all values + """ + normalizedCounter = Counter() + if type(vectorOrCounter) == type(normalizedCounter): + counter = vectorOrCounter + total = float(counter.totalCount()) + if total == 0: return counter + for key in counter.keys(): + value = counter[key] + normalizedCounter[key] = value / total + return normalizedCounter + else: + vector = vectorOrCounter + s = float(sum(vector)) + if s == 0: return vector + return [el / s for el in vector] + +def nSample(distribution, values, n): + if sum(distribution) != 1: + distribution = normalize(distribution) + rand = [random.random() for i in range(n)] + rand.sort() + samples = [] + samplePos, distPos, cdf = 0,0, distribution[0] + while samplePos < n: + if rand[samplePos] < cdf: + samplePos += 1 + samples.append(values[distPos]) + else: + distPos += 1 + cdf += distribution[distPos] + return samples + +def sample(distribution, values = None): + if type(distribution) == Counter: + items = sorted(distribution.items()) + distribution = [i[1] for i in items] + values = [i[0] for i in items] + if sum(distribution) != 1: + distribution = normalize(distribution) + choice = random.random() + i, total= 0, distribution[0] + while choice > total: + i += 1 + total += distribution[i] + return values[i] + +def sampleFromCounter(ctr): + items = sorted(ctr.items()) + return sample([v for k,v in items], [k for k,v in items]) + +def getProbability(value, distribution, values): + """ + Gives the probability of a value under a discrete distribution + defined by (distributions, values). + """ + total = 0.0 + for prob, val in zip(distribution, values): + if val == value: + total += prob + return total + +def flipCoin( p ): + r = random.random() + return r < p + +def chooseFromDistribution( distribution ): + "Takes either a counter or a list of (prob, key) pairs and samples" + if type(distribution) == dict or type(distribution) == Counter: + return sample(distribution) + r = random.random() + base = 0.0 + for prob, element in distribution: + base += prob + if r <= base: return element + +def nearestPoint( pos ): + """ + Finds the nearest grid point to a position (discretizes). + """ + ( current_row, current_col ) = pos + + grid_row = int( current_row + 0.5 ) + grid_col = int( current_col + 0.5 ) + return ( grid_row, grid_col ) + +def sign( x ): + """ + Returns 1 or -1 depending on the sign of x + """ + if( x >= 0 ): + return 1 + else: + return -1 + +def arrayInvert(array): + """ + Inverts a matrix stored as a list of lists. + """ + result = [[] for i in array] + for outer in array: + for inner in range(len(outer)): + result[inner].append(outer[inner]) + return result + +def matrixAsList( matrix, value = True ): + """ + Turns a matrix into a list of coordinates matching the specified value + """ + rows, cols = len( matrix ), len( matrix[0] ) + cells = [] + for row in range( rows ): + for col in range( cols ): + if matrix[row][col] == value: + cells.append( ( row, col ) ) + return cells + +def lookup(name, namespace): + """ + Get a method or class from any imported module from its name. + Usage: lookup(functionName, globals()) + """ + dots = name.count('.') + if dots > 0: + moduleName, objName = '.'.join(name.split('.')[:-1]), name.split('.')[-1] + module = __import__(moduleName) + return getattr(module, objName) + else: + modules = [obj for obj in namespace.values() if str(type(obj)) == ""] + options = [getattr(module, name) for module in modules if name in dir(module)] + options += [obj[1] for obj in namespace.items() if obj[0] == name ] + if len(options) == 1: return options[0] + if len(options) > 1: raise Exception, 'Name conflict for %s' + raise Exception, '%s not found as a method or class' % name + +def pause(): + """ + Pauses the output stream awaiting user feedback. + """ + print "" + raw_input() + + +# code to handle timeouts +# +# FIXME +# NOTE: TimeoutFuncton is NOT reentrant. Later timeouts will silently +# disable earlier timeouts. Could be solved by maintaining a global list +# of active time outs. Currently, questions which have test cases calling +# this have all student code so wrapped. +# +import signal +import time +class TimeoutFunctionException(Exception): + """Exception to raise on a timeout""" + pass + + +class TimeoutFunction: + def __init__(self, function, timeout): + self.timeout = timeout + self.function = function + + def handle_timeout(self, signum, frame): + raise TimeoutFunctionException() + + def __call__(self, *args, **keyArgs): + # If we have SIGALRM signal, use it to cause an exception if and + # when this function runs too long. Otherwise check the time taken + # after the method has returned, and throw an exception then. + if hasattr(signal, 'SIGALRM'): + old = signal.signal(signal.SIGALRM, self.handle_timeout) + signal.alarm(self.timeout) + try: + result = self.function(*args, **keyArgs) + finally: + signal.signal(signal.SIGALRM, old) + signal.alarm(0) + else: + startTime = time.time() + result = self.function(*args, **keyArgs) + timeElapsed = time.time() - startTime + if timeElapsed >= self.timeout: + self.handle_timeout(None, None) + return result + + + +_ORIGINAL_STDOUT = None +_ORIGINAL_STDERR = None +_MUTED = False + +class WritableNull: + def write(self, string): + pass + +def mutePrint(): + global _ORIGINAL_STDOUT, _ORIGINAL_STDERR, _MUTED + if _MUTED: + return + _MUTED = True + + _ORIGINAL_STDOUT = sys.stdout + #_ORIGINAL_STDERR = sys.stderr + sys.stdout = WritableNull() + #sys.stderr = WritableNull() + +def unmutePrint(): + global _ORIGINAL_STDOUT, _ORIGINAL_STDERR, _MUTED + if not _MUTED: + return + _MUTED = False + + sys.stdout = _ORIGINAL_STDOUT + #sys.stderr = _ORIGINAL_STDERR +