From 2c9fa8ff49ce1768b46afaaf468545ffa6d5ff01 Mon Sep 17 00:00:00 2001 From: Juhani Numminen Date: Thu, 5 Jan 2017 20:14:45 +0000 Subject: [PATCH] Import pentobi_12.2.orig.tar.xz [dgit import orig pentobi_12.2.orig.tar.xz] --- .gitignore | 2 + CMakeLists.txt | 106 + COPYING | 694 ++++ INSTALL | 60 + NEWS | 582 +++ README | 9 + config.h.in | 30 + data/CMakeLists.txt | 51 + data/application-x-blokus-sgf-16.svg | 13 + data/application-x-blokus-sgf-32.svg | 33 + data/application-x-blokus-sgf-64.svg | 37 + data/application-x-blokus-sgf.svg | 37 + data/pentobi-mime.xml | 23 + data/pentobi.appdata.xml.in | 91 + data/pentobi.desktop.in | 12 + data/pentobi.thumbnailer.in | 3 + doc/CMakeLists.txt | 1 + doc/blksgf/Pentobi-SGF.html | 147 + doc/doxygen/.gitignore | 2 + doc/doxygen/Doxyfile | 317 ++ doc/doxygen/footer.html | 8 + doc/gtp/Pentobi-GTP.html | 321 ++ doc/man/CMakeLists.txt | 6 + doc/man/pentobi-thumbnailer.6.in | 35 + doc/man/pentobi.6.in | 49 + src/CMakeLists.txt | 40 + src/books/CMakeLists.txt | 17 + src/books/book_callisto.blksgf | 16 + src/books/book_callisto_2.blksgf | 24 + src/books/book_callisto_3.blksgf | 21 + src/books/book_classic.blksgf | 14 + src/books/book_classic_2.blksgf | 891 +++++ src/books/book_classic_3.blksgf | 14 + src/books/book_duo.blksgf | 212 + src/books/book_junior.blksgf | 9 + src/books/book_nexos.blksgf | 15 + src/books/book_nexos_2.blksgf | 15 + src/books/book_trigon.blksgf | 9 + src/books/book_trigon_2.blksgf | 25 + src/books/book_trigon_3.blksgf | 9 + src/books/pentobi_books.qrc | 17 + src/convert/CMakeLists.txt | 3 + src/convert/Main.cpp | 54 + src/doc_libboardgame.cpp | 76 + src/doc_mainpage.cpp | 72 + src/libboardgame_base/CMakeLists.txt | 22 + src/libboardgame_base/CoordPoint.cpp | 30 + src/libboardgame_base/CoordPoint.h | 128 + src/libboardgame_base/Engine.cpp | 57 + src/libboardgame_base/Engine.h | 38 + src/libboardgame_base/Geometry.h | 343 ++ src/libboardgame_base/GeometryUtil.h | 89 + src/libboardgame_base/Grid.h | 226 ++ src/libboardgame_base/Marker.h | 101 + src/libboardgame_base/Point.h | 149 + src/libboardgame_base/PointTransform.h | 429 ++ src/libboardgame_base/Rating.cpp | 52 + src/libboardgame_base/Rating.h | 72 + src/libboardgame_base/RectGeometry.h | 137 + src/libboardgame_base/RectTransform.cpp | 73 + src/libboardgame_base/RectTransform.h | 106 + src/libboardgame_base/StringRep.cpp | 86 + src/libboardgame_base/StringRep.h | 55 + src/libboardgame_base/Transform.cpp | 23 + src/libboardgame_base/Transform.h | 55 + src/libboardgame_gtp/Arguments.cpp | 84 + src/libboardgame_gtp/Arguments.h | 240 ++ src/libboardgame_gtp/CMakeLists.txt | 12 + src/libboardgame_gtp/CmdLine.cpp | 116 + src/libboardgame_gtp/CmdLine.h | 118 + src/libboardgame_gtp/CmdLineRange.h | 86 + src/libboardgame_gtp/Engine.cpp | 243 ++ src/libboardgame_gtp/Engine.h | 227 ++ src/libboardgame_gtp/Failure.h | 31 + src/libboardgame_gtp/Response.cpp | 40 + src/libboardgame_gtp/Response.h | 86 + src/libboardgame_mcts/Atomic.h | 101 + src/libboardgame_mcts/CMakeLists.txt | 11 + src/libboardgame_mcts/LastGoodReply.h | 154 + src/libboardgame_mcts/Node.h | 292 ++ src/libboardgame_mcts/PlayerMove.h | 40 + src/libboardgame_mcts/SearchBase.h | 1544 ++++++++ src/libboardgame_mcts/Tree.h | 474 +++ src/libboardgame_mcts/TreeUtil.h | 41 + src/libboardgame_sgf/CMakeLists.txt | 20 + src/libboardgame_sgf/InvalidPropertyValue.h | 50 + src/libboardgame_sgf/InvalidTree.h | 37 + src/libboardgame_sgf/MissingProperty.cpp | 29 + src/libboardgame_sgf/MissingProperty.h | 31 + src/libboardgame_sgf/Reader.cpp | 261 ++ src/libboardgame_sgf/Reader.h | 110 + src/libboardgame_sgf/SgfNode.cpp | 298 ++ src/libboardgame_sgf/SgfNode.h | 393 ++ src/libboardgame_sgf/SgfTree.cpp | 265 ++ src/libboardgame_sgf/SgfTree.h | 279 ++ src/libboardgame_sgf/SgfUtil.cpp | 221 ++ src/libboardgame_sgf/SgfUtil.h | 77 + src/libboardgame_sgf/TreeReader.cpp | 65 + src/libboardgame_sgf/TreeReader.h | 62 + src/libboardgame_sgf/TreeWriter.cpp | 60 + src/libboardgame_sgf/TreeWriter.h | 73 + src/libboardgame_sgf/Writer.cpp | 84 + src/libboardgame_sgf/Writer.h | 139 + src/libboardgame_sys/CMakeLists.txt | 7 + src/libboardgame_sys/Compiler.h | 66 + src/libboardgame_sys/CpuTime.cpp | 65 + src/libboardgame_sys/CpuTime.h | 23 + src/libboardgame_sys/Memory.cpp | 70 + src/libboardgame_sys/Memory.h | 26 + src/libboardgame_test/CMakeLists.txt | 4 + src/libboardgame_test/Test.cpp | 118 + src/libboardgame_test/Test.h | 153 + src/libboardgame_test_main/CMakeLists.txt | 1 + src/libboardgame_test_main/Main.cpp | 16 + src/libboardgame_util/Abort.cpp | 23 + src/libboardgame_util/Abort.h | 40 + src/libboardgame_util/ArrayList.h | 353 ++ src/libboardgame_util/Assert.cpp | 74 + src/libboardgame_util/Assert.h | 57 + src/libboardgame_util/Barrier.cpp | 43 + src/libboardgame_util/Barrier.h | 43 + src/libboardgame_util/CMakeLists.txt | 34 + src/libboardgame_util/CpuTimeSource.cpp | 26 + src/libboardgame_util/CpuTimeSource.h | 28 + src/libboardgame_util/FmtSaver.h | 44 + src/libboardgame_util/IntervalChecker.cpp | 104 + src/libboardgame_util/IntervalChecker.h | 79 + src/libboardgame_util/Log.cpp | 124 + src/libboardgame_util/Log.h | 130 + src/libboardgame_util/MathUtil.h | 37 + src/libboardgame_util/Options.cpp | 161 + src/libboardgame_util/Options.h | 124 + src/libboardgame_util/RandomGenerator.cpp | 74 + src/libboardgame_util/RandomGenerator.h | 99 + src/libboardgame_util/Range.h | 52 + src/libboardgame_util/Statistics.h | 457 +++ src/libboardgame_util/StringUtil.cpp | 105 + src/libboardgame_util/StringUtil.h | 58 + src/libboardgame_util/TimeIntervalChecker.cpp | 43 + src/libboardgame_util/TimeIntervalChecker.h | 41 + src/libboardgame_util/TimeSource.cpp | 23 + src/libboardgame_util/TimeSource.h | 33 + src/libboardgame_util/Timer.cpp | 43 + src/libboardgame_util/Timer.h | 42 + src/libboardgame_util/Unused.h | 22 + src/libboardgame_util/WallTimeSource.cpp | 29 + src/libboardgame_util/WallTimeSource.h | 28 + src/libpentobi_base/Board.cpp | 799 ++++ src/libpentobi_base/Board.h | 901 +++++ src/libpentobi_base/BoardConst.cpp | 1128 ++++++ src/libpentobi_base/BoardConst.h | 363 ++ src/libpentobi_base/BoardUpdater.cpp | 154 + src/libpentobi_base/BoardUpdater.h | 36 + src/libpentobi_base/BoardUtil.cpp | 91 + src/libpentobi_base/BoardUtil.h | 40 + src/libpentobi_base/Book.cpp | 144 + src/libpentobi_base/Book.h | 69 + src/libpentobi_base/CMakeLists.txt | 78 + src/libpentobi_base/CallistoGeometry.cpp | 125 + src/libpentobi_base/CallistoGeometry.h | 63 + src/libpentobi_base/Color.cpp | 72 + src/libpentobi_base/Color.h | 190 + src/libpentobi_base/ColorMap.h | 69 + src/libpentobi_base/ColorMove.h | 80 + src/libpentobi_base/Engine.cpp | 379 ++ src/libpentobi_base/Engine.h | 104 + src/libpentobi_base/Game.cpp | 191 + src/libpentobi_base/Game.h | 429 ++ src/libpentobi_base/Geometry.h | 23 + src/libpentobi_base/Grid.h | 27 + src/libpentobi_base/Marker.h | 23 + src/libpentobi_base/Move.h | 136 + src/libpentobi_base/MoveInfo.h | 135 + src/libpentobi_base/MoveList.h | 24 + src/libpentobi_base/MoveMarker.h | 72 + src/libpentobi_base/MovePoints.h | 28 + src/libpentobi_base/NexosGeometry.cpp | 94 + src/libpentobi_base/NexosGeometry.h | 74 + src/libpentobi_base/NodeUtil.cpp | 175 + src/libpentobi_base/NodeUtil.h | 43 + src/libpentobi_base/PentobiSgfUtil.cpp | 53 + src/libpentobi_base/PentobiSgfUtil.h | 29 + src/libpentobi_base/PentobiTree.cpp | 365 ++ src/libpentobi_base/PentobiTree.h | 168 + src/libpentobi_base/PentobiTreeWriter.cpp | 82 + src/libpentobi_base/PentobiTreeWriter.h | 39 + src/libpentobi_base/Piece.h | 110 + src/libpentobi_base/PieceInfo.cpp | 231 ++ src/libpentobi_base/PieceInfo.h | 137 + src/libpentobi_base/PieceMap.h | 85 + src/libpentobi_base/PieceTransforms.cpp | 21 + src/libpentobi_base/PieceTransforms.h | 77 + .../PieceTransformsClassic.cpp | 146 + src/libpentobi_base/PieceTransformsClassic.h | 66 + src/libpentobi_base/PieceTransformsTrigon.cpp | 187 + src/libpentobi_base/PieceTransformsTrigon.h | 67 + src/libpentobi_base/PlayerBase.cpp | 28 + src/libpentobi_base/PlayerBase.h | 34 + src/libpentobi_base/Point.h | 27 + src/libpentobi_base/PointList.h | 23 + src/libpentobi_base/PointState.cpp | 30 + src/libpentobi_base/PointState.h | 142 + src/libpentobi_base/PrecompMoves.h | 136 + src/libpentobi_base/ScoreUtil.h | 66 + src/libpentobi_base/Setup.h | 44 + src/libpentobi_base/StartingPoints.cpp | 99 + src/libpentobi_base/StartingPoints.h | 81 + src/libpentobi_base/SymmetricPoints.cpp | 31 + src/libpentobi_base/SymmetricPoints.h | 39 + src/libpentobi_base/TreeUtil.cpp | 74 + src/libpentobi_base/TreeUtil.h | 39 + src/libpentobi_base/TrigonGeometry.cpp | 128 + src/libpentobi_base/TrigonGeometry.h | 69 + src/libpentobi_base/TrigonTransform.cpp | 135 + src/libpentobi_base/TrigonTransform.h | 153 + src/libpentobi_base/Variant.cpp | 442 +++ src/libpentobi_base/Variant.h | 150 + src/libpentobi_gui/BoardPainter.cpp | 620 +++ src/libpentobi_gui/BoardPainter.h | 147 + src/libpentobi_gui/CMakeLists.txt | 87 + src/libpentobi_gui/ComputerColorDialog.cpp | 85 + src/libpentobi_gui/ComputerColorDialog.h | 54 + src/libpentobi_gui/GameInfoDialog.cpp | 149 + src/libpentobi_gui/GameInfoDialog.h | 75 + src/libpentobi_gui/GuiBoard.cpp | 520 +++ src/libpentobi_gui/GuiBoard.h | 188 + src/libpentobi_gui/GuiBoardUtil.cpp | 115 + src/libpentobi_gui/GuiBoardUtil.h | 27 + src/libpentobi_gui/HelpWindow.cpp | 118 + src/libpentobi_gui/HelpWindow.h | 52 + src/libpentobi_gui/InitialRatingDialog.cpp | 61 + src/libpentobi_gui/InitialRatingDialog.h | 53 + src/libpentobi_gui/LeaveFullscreenButton.cpp | 79 + src/libpentobi_gui/LeaveFullscreenButton.h | 68 + src/libpentobi_gui/LineEdit.cpp | 32 + src/libpentobi_gui/LineEdit.h | 37 + src/libpentobi_gui/OrientationDisplay.cpp | 218 ++ src/libpentobi_gui/OrientationDisplay.h | 68 + src/libpentobi_gui/PieceSelector.cpp | 380 ++ src/libpentobi_gui/PieceSelector.h | 94 + src/libpentobi_gui/SameHeightLayout.cpp | 116 + src/libpentobi_gui/SameHeightLayout.h | 55 + src/libpentobi_gui/ScoreDisplay.cpp | 314 ++ src/libpentobi_gui/ScoreDisplay.h | 94 + src/libpentobi_gui/Util.cpp | 531 +++ src/libpentobi_gui/Util.h | 112 + src/libpentobi_gui/icons/go-home.svg | 8 + src/libpentobi_gui/icons/go-next.svg | 4 + src/libpentobi_gui/icons/go-previous.svg | 4 + .../libpentobi_gui_resources.qrc | 8 + .../libpentobi_gui_resources_2x.qrc | 8 + .../translations/libpentobi_gui_de.ts | 170 + src/libpentobi_kde_thumbnailer/CMakeLists.txt | 38 + src/libpentobi_mcts/AnalyzeGame.cpp | 108 + src/libpentobi_mcts/AnalyzeGame.h | 83 + src/libpentobi_mcts/CMakeLists.txt | 24 + src/libpentobi_mcts/Float.h | 28 + src/libpentobi_mcts/History.cpp | 77 + src/libpentobi_mcts/History.h | 105 + src/libpentobi_mcts/Player.cpp | 366 ++ src/libpentobi_mcts/Player.h | 193 + src/libpentobi_mcts/PlayoutFeatures.cpp | 21 + src/libpentobi_mcts/PlayoutFeatures.h | 215 + src/libpentobi_mcts/PriorKnowledge.cpp | 132 + src/libpentobi_mcts/PriorKnowledge.h | 432 ++ src/libpentobi_mcts/Search.cpp | 171 + src/libpentobi_mcts/Search.h | 162 + src/libpentobi_mcts/SearchParamConst.h | 75 + src/libpentobi_mcts/SharedConst.cpp | 317 ++ src/libpentobi_mcts/SharedConst.h | 96 + src/libpentobi_mcts/State.cpp | 873 +++++ src/libpentobi_mcts/State.h | 545 +++ src/libpentobi_mcts/StateUtil.cpp | 100 + src/libpentobi_mcts/StateUtil.h | 25 + src/libpentobi_mcts/Util.cpp | 118 + src/libpentobi_mcts/Util.h | 35 + src/libpentobi_thumbnail/CMakeLists.txt | 6 + src/libpentobi_thumbnail/CreateThumbnail.cpp | 157 + src/libpentobi_thumbnail/CreateThumbnail.h | 20 + src/pentobi/AnalyzeGameWidget.cpp | 231 ++ src/pentobi/AnalyzeGameWidget.h | 117 + src/pentobi/AnalyzeGameWindow.cpp | 33 + src/pentobi/AnalyzeGameWindow.h | 36 + src/pentobi/AnalyzeSpeedDialog.cpp | 37 + src/pentobi/AnalyzeSpeedDialog.h | 44 + src/pentobi/Application.cpp | 35 + src/pentobi/Application.h | 34 + src/pentobi/CMakeLists.txt | 142 + src/pentobi/Main.cpp | 210 + src/pentobi/MainWindow.cpp | 3466 +++++++++++++++++ src/pentobi/MainWindow.h | 707 ++++ src/pentobi/RatedGamesList.cpp | 127 + src/pentobi/RatedGamesList.h | 51 + src/pentobi/RatingDialog.cpp | 172 + src/pentobi/RatingDialog.h | 69 + src/pentobi/RatingGraph.cpp | 121 + src/pentobi/RatingGraph.h | 45 + src/pentobi/RatingHistory.cpp | 194 + src/pentobi/RatingHistory.h | 140 + src/pentobi/ShowMessage.cpp | 76 + src/pentobi/ShowMessage.h | 31 + src/pentobi/Util.cpp | 72 + src/pentobi/Util.h | 49 + src/pentobi/help/C/pentobi/analysis.jpg | Bin 0 -> 4625 bytes .../help/C/pentobi/become_stronger.html | 60 + src/pentobi/help/C/pentobi/board_callisto.png | Bin 0 -> 12907 bytes src/pentobi/help/C/pentobi/board_classic.png | Bin 0 -> 1276 bytes src/pentobi/help/C/pentobi/board_duo.png | Bin 0 -> 660 bytes src/pentobi/help/C/pentobi/board_nexos.png | Bin 0 -> 945 bytes src/pentobi/help/C/pentobi/board_trigon.jpg | Bin 0 -> 13242 bytes .../help/C/pentobi/callisto_rules.html | 45 + src/pentobi/help/C/pentobi/classic_rules.html | 63 + src/pentobi/help/C/pentobi/duo_rules.html | 28 + src/pentobi/help/C/pentobi/index.html | 28 + src/pentobi/help/C/pentobi/junior_rules.html | 22 + src/pentobi/help/C/pentobi/license.html | 26 + src/pentobi/help/C/pentobi/nexos_rules.html | 44 + src/pentobi/help/C/pentobi/pieces.png | Bin 0 -> 6338 bytes .../help/C/pentobi/pieces_callisto.png | Bin 0 -> 3898 bytes src/pentobi/help/C/pentobi/pieces_junior.png | Bin 0 -> 7691 bytes src/pentobi/help/C/pentobi/pieces_nexos.png | Bin 0 -> 7987 bytes src/pentobi/help/C/pentobi/pieces_trigon.jpg | Bin 0 -> 8059 bytes .../help/C/pentobi/position_callisto.png | Bin 0 -> 15658 bytes .../help/C/pentobi/position_classic.png | Bin 0 -> 5018 bytes src/pentobi/help/C/pentobi/position_duo.png | Bin 0 -> 8340 bytes src/pentobi/help/C/pentobi/position_nexos.png | Bin 0 -> 2057 bytes .../help/C/pentobi/position_trigon.jpg | Bin 0 -> 20020 bytes src/pentobi/help/C/pentobi/rating.jpg | Bin 0 -> 7637 bytes src/pentobi/help/C/pentobi/shortcuts.html | 58 + src/pentobi/help/C/pentobi/stylesheet.css | 20 + src/pentobi/help/C/pentobi/system.html | 22 + src/pentobi/help/C/pentobi/trigon_rules.html | 44 + .../help/C/pentobi/user_interface.html | 72 + src/pentobi/help/C/pentobi/window_menu.html | 201 + .../help/de/pentobi/become_stronger.html | 67 + .../help/de/pentobi/callisto_rules.html | 50 + .../help/de/pentobi/classic_rules.html | 65 + src/pentobi/help/de/pentobi/duo_rules.html | 29 + src/pentobi/help/de/pentobi/index.html | 29 + src/pentobi/help/de/pentobi/junior_rules.html | 24 + src/pentobi/help/de/pentobi/license.html | 26 + src/pentobi/help/de/pentobi/nexos_rules.html | 47 + src/pentobi/help/de/pentobi/shortcuts.html | 59 + src/pentobi/help/de/pentobi/system.html | 22 + src/pentobi/help/de/pentobi/trigon_rules.html | 47 + .../help/de/pentobi/user_interface.html | 80 + src/pentobi/help/de/pentobi/window_menu.html | 226 ++ src/pentobi/icons.qrc | 15 + src/pentobi/icons/pentobi-16.svg | 31 + src/pentobi/icons/pentobi-32.svg | 35 + src/pentobi/icons/pentobi-64.svg | 35 + src/pentobi/icons/pentobi-backward-16.svg | 5 + src/pentobi/icons/pentobi-backward.svg | 5 + src/pentobi/icons/pentobi-beginning-16.svg | 5 + src/pentobi/icons/pentobi-beginning.svg | 5 + .../icons/pentobi-computer-colors-16.svg | 18 + src/pentobi/icons/pentobi-computer-colors.svg | 18 + src/pentobi/icons/pentobi-end-16.svg | 5 + src/pentobi/icons/pentobi-end.svg | 5 + src/pentobi/icons/pentobi-flip-horizontal.svg | 6 + src/pentobi/icons/pentobi-flip-vertical.svg | 6 + src/pentobi/icons/pentobi-forward-16.svg | 5 + src/pentobi/icons/pentobi-forward.svg | 5 + src/pentobi/icons/pentobi-newgame-16.svg | 13 + src/pentobi/icons/pentobi-newgame.svg | 13 + src/pentobi/icons/pentobi-next-piece.svg | 6 + .../icons/pentobi-next-variation-16.svg | 5 + src/pentobi/icons/pentobi-next-variation.svg | 5 + src/pentobi/icons/pentobi-piece-clear.svg | 6 + src/pentobi/icons/pentobi-play-16.svg | 14 + src/pentobi/icons/pentobi-play.svg | 14 + src/pentobi/icons/pentobi-previous-piece.svg | 6 + .../icons/pentobi-previous-variation-16.svg | 5 + .../icons/pentobi-previous-variation.svg | 5 + src/pentobi/icons/pentobi-rated-game-16.svg | 8 + src/pentobi/icons/pentobi-rated-game.svg | 8 + src/pentobi/icons/pentobi-rotate-left.svg | 4 + src/pentobi/icons/pentobi-rotate-right.svg | 4 + src/pentobi/icons/pentobi-undo-16.svg | 13 + src/pentobi/icons/pentobi-undo.svg | 13 + src/pentobi/icons/pentobi.svg | 35 + src/pentobi/pentobi.conf.in | 6 + src/pentobi/pentobi.ico | Bin 0 -> 8216 bytes src/pentobi/pentobi.rc | 1 + src/pentobi/resources.qrc | 37 + src/pentobi/resources_2x.qrc | 37 + src/pentobi/translations/pentobi.ts | 14 + src/pentobi/translations/pentobi_de.ts | 1116 ++++++ src/pentobi_gtp/CMakeLists.txt | 24 + src/pentobi_gtp/Engine.cpp | 183 + src/pentobi_gtp/Engine.h | 60 + src/pentobi_gtp/Main.cpp | 170 + src/pentobi_kde_thumbnailer/CMakeLists.txt | 23 + .../PentobiThumbCreator.cpp | 34 + .../PentobiThumbCreator.h | 30 + .../pentobi-thumbnail.desktop | 7 + src/pentobi_qml/.gitignore | 1 + src/pentobi_qml/CMakeLists.txt | 41 + src/pentobi_qml/GameModel.cpp | 807 ++++ src/pentobi_qml/GameModel.h | 325 ++ src/pentobi_qml/Main.cpp | 106 + src/pentobi_qml/Pentobi.pro | 225 ++ src/pentobi_qml/PieceModel.cpp | 373 ++ src/pentobi_qml/PieceModel.h | 133 + src/pentobi_qml/PlayerModel.cpp | 228 ++ src/pentobi_qml/PlayerModel.h | 173 + src/pentobi_qml/android/AndroidManifest.xml | 49 + .../android/res/drawable-hdpi/icon.png | Bin 0 -> 409 bytes .../android/res/drawable-mdpi/icon.png | Bin 0 -> 324 bytes .../android/res/drawable/splash.xml | 11 + src/pentobi_qml/android/res/values/theme.xml | 6 + src/pentobi_qml/android_icons_svg/icon48.svg | 35 + src/pentobi_qml/android_icons_svg/icon72.svg | 35 + src/pentobi_qml/deployment.pri | 27 + src/pentobi_qml/icons_android.qrc | 15 + src/pentobi_qml/qml/.gitignore | 1 + src/pentobi_qml/qml/AndroidToolBar.qml | 46 + src/pentobi_qml/qml/AndroidToolButton.qml | 18 + src/pentobi_qml/qml/Board.qml | 208 + src/pentobi_qml/qml/Button.qml | 29 + src/pentobi_qml/qml/ComputerColorDialog.qml | 82 + src/pentobi_qml/qml/GameDisplay.js | 108 + src/pentobi_qml/qml/GameDisplay.qml | 157 + src/pentobi_qml/qml/LineSegment.qml | 105 + src/pentobi_qml/qml/Main.js | 274 ++ src/pentobi_qml/qml/Main.qml | 234 ++ src/pentobi_qml/qml/MenuComputer.qml | 47 + src/pentobi_qml/qml/MenuEdit.qml | 50 + src/pentobi_qml/qml/MenuGame.qml | 119 + src/pentobi_qml/qml/MenuGo.qml | 17 + src/pentobi_qml/qml/MenuHelp.qml | 12 + src/pentobi_qml/qml/MenuItemGameVariant.qml | 12 + src/pentobi_qml/qml/MenuItemLevel.qml | 43 + src/pentobi_qml/qml/MenuView.qml | 19 + src/pentobi_qml/qml/NavigationPanel.qml | 57 + src/pentobi_qml/qml/OpenDialog.qml | 15 + src/pentobi_qml/qml/PieceCallisto.qml | 293 ++ src/pentobi_qml/qml/PieceClassic.qml | 259 ++ src/pentobi_qml/qml/PieceFlipAnimation.qml | 7 + src/pentobi_qml/qml/PieceList.qml | 26 + src/pentobi_qml/qml/PieceManipulator.qml | 72 + src/pentobi_qml/qml/PieceNexos.qml | 310 ++ .../qml/PieceRotationAnimation.qml | 7 + src/pentobi_qml/qml/PieceSelector.qml | 195 + src/pentobi_qml/qml/PieceTrigon.qml | 320 ++ src/pentobi_qml/qml/SaveDialog.qml | 16 + src/pentobi_qml/qml/ScoreDisplay.qml | 110 + src/pentobi_qml/qml/ScoreElement.qml | 40 + src/pentobi_qml/qml/ScoreElement2.qml | 58 + src/pentobi_qml/qml/Square.qml | 100 + src/pentobi_qml/qml/ToolBar.qml | 30 + src/pentobi_qml/qml/Triangle.qml | 176 + src/pentobi_qml/qml/i18n/qml_de.ts | 480 +++ src/pentobi_qml/qml/i18n/replace_qtbase_de.ts | 11 + src/pentobi_qml/qml/icons/menu.svg | 6 + .../qml/icons/pentobi-backward.svg | 4 + .../qml/icons/pentobi-beginning.svg | 4 + .../qml/icons/pentobi-computer-colors.svg | 4 + src/pentobi_qml/qml/icons/pentobi-end.svg | 4 + src/pentobi_qml/qml/icons/pentobi-forward.svg | 4 + src/pentobi_qml/qml/icons/pentobi-newgame.svg | 4 + .../qml/icons/pentobi-next-variation.svg | 4 + src/pentobi_qml/qml/icons/pentobi-play.svg | 4 + .../qml/icons/pentobi-previous-variation.svg | 4 + src/pentobi_qml/qml/icons/pentobi-undo.svg | 4 + src/pentobi_qml/qml/themes/dark/Theme.qml | 26 + .../qml/themes/dark/board-callisto-2.svg | 156 + .../qml/themes/dark/board-callisto-3.svg | 232 ++ .../qml/themes/dark/board-callisto.svg | 300 ++ .../qml/themes/dark/board-tile-classic.svg | 6 + .../qml/themes/dark/board-tile-nexos.svg | 9 + .../qml/themes/dark/board-trigon-3.svg | 394 ++ .../qml/themes/dark/board-trigon.svg | 496 +++ src/pentobi_qml/qml/themes/light/Theme.qml | 17 + .../qml/themes/light/board-callisto-2.svg | 156 + .../qml/themes/light/board-callisto-3.svg | 232 ++ .../qml/themes/light/board-callisto.svg | 300 ++ .../qml/themes/light/board-tile-classic.svg | 6 + .../qml/themes/light/board-tile-nexos.svg | 9 + .../qml/themes/light/board-trigon-3.svg | 394 ++ .../qml/themes/light/board-trigon.svg | 496 +++ .../qml/themes/light/frame-blue.svg | 6 + .../qml/themes/light/frame-green.svg | 6 + .../qml/themes/light/frame-red.svg | 6 + .../qml/themes/light/frame-yellow.svg | 6 + .../qml/themes/light/junction-all-blue.svg | 4 + .../qml/themes/light/junction-all-green.svg | 4 + .../qml/themes/light/junction-all-red.svg | 4 + .../qml/themes/light/junction-all-yellow.svg | 4 + .../qml/themes/light/junction-rect-blue.svg | 4 + .../qml/themes/light/junction-rect-green.svg | 4 + .../qml/themes/light/junction-rect-red.svg | 4 + .../qml/themes/light/junction-rect-yellow.svg | 4 + .../themes/light/junction-straight-blue.svg | 4 + .../themes/light/junction-straight-green.svg | 4 + .../themes/light/junction-straight-red.svg | 4 + .../themes/light/junction-straight-yellow.svg | 4 + .../qml/themes/light/junction-t-blue.svg | 4 + .../qml/themes/light/junction-t-green.svg | 4 + .../qml/themes/light/junction-t-red.svg | 4 + .../qml/themes/light/junction-t-yellow.svg | 4 + .../qml/themes/light/linesegment-blue.svg | 6 + .../qml/themes/light/linesegment-green.svg | 6 + .../qml/themes/light/linesegment-red.svg | 6 + .../qml/themes/light/linesegment-yellow.svg | 6 + .../themes/light/piece-manipulator-legal.svg | 17 + .../qml/themes/light/piece-manipulator.svg | 15 + .../qml/themes/light/square-blue.svg | 6 + .../qml/themes/light/square-green.svg | 6 + .../qml/themes/light/square-red.svg | 6 + .../qml/themes/light/square-yellow.svg | 6 + .../qml/themes/light/triangle-blue.svg | 7 + .../qml/themes/light/triangle-down-blue.svg | 7 + .../qml/themes/light/triangle-down-green.svg | 7 + .../qml/themes/light/triangle-down-red.svg | 7 + .../qml/themes/light/triangle-down-yellow.svg | 7 + .../qml/themes/light/triangle-green.svg | 7 + .../qml/themes/light/triangle-red.svg | 7 + .../qml/themes/light/triangle-yellow.svg | 7 + src/pentobi_qml/qml/themes/theme_dark.qrc | 12 + src/pentobi_qml/qml/themes/theme_light.qrc | 12 + src/pentobi_qml/qml/themes/theme_shared.qrc | 42 + src/pentobi_qml/resources.qrc | 40 + src/pentobi_qml/translations.qrc | 6 + src/pentobi_thumbnailer/CMakeLists.txt | 17 + src/pentobi_thumbnailer/Main.cpp | 55 + src/twogtp/Analyze.cpp | 173 + src/twogtp/Analyze.h | 22 + src/twogtp/CMakeLists.txt | 28 + src/twogtp/FdStream.cpp | 93 + src/twogtp/FdStream.h | 85 + src/twogtp/GtpConnection.cpp | 175 + src/twogtp/GtpConnection.h | 53 + src/twogtp/Main.cpp | 105 + src/twogtp/Output.cpp | 128 + src/twogtp/Output.h | 55 + src/twogtp/OutputTree.cpp | 241 ++ src/twogtp/OutputTree.h | 82 + src/twogtp/TwoGtp.cpp | 195 + src/twogtp/TwoGtp.h | 61 + src/unittest/CMakeLists.txt | 10 + src/unittest/libboardgame_base/CMakeLists.txt | 17 + src/unittest/libboardgame_base/MarkerTest.cpp | 61 + .../libboardgame_base/PointTransformTest.cpp | 46 + src/unittest/libboardgame_base/RatingTest.cpp | 70 + .../libboardgame_base/RectGeometryTest.cpp | 90 + .../libboardgame_base/StringRepTest.cpp | 86 + .../libboardgame_gtp/ArgumentsTest.cpp | 157 + src/unittest/libboardgame_gtp/CMakeLists.txt | 15 + src/unittest/libboardgame_gtp/CmdLineTest.cpp | 70 + src/unittest/libboardgame_gtp/EngineTest.cpp | 121 + .../libboardgame_gtp/ResponseTest.cpp | 28 + src/unittest/libboardgame_mcts/CMakeLists.txt | 13 + src/unittest/libboardgame_mcts/NodeTest.cpp | 41 + src/unittest/libboardgame_sgf/CMakeLists.txt | 14 + src/unittest/libboardgame_sgf/SgfNodeTest.cpp | 41 + src/unittest/libboardgame_sgf/SgfUtilTest.cpp | 32 + .../libboardgame_sgf/TreeReaderTest.cpp | 122 + .../libboardgame_util/ArrayListTest.cpp | 74 + src/unittest/libboardgame_util/CMakeLists.txt | 15 + .../libboardgame_util/OptionsTest.cpp | 91 + .../libboardgame_util/StatisticsTest.cpp | 33 + .../libboardgame_util/StringUtilTest.cpp | 97 + .../libpentobi_base/BoardConstTest.cpp | 42 + src/unittest/libpentobi_base/BoardTest.cpp | 165 + .../libpentobi_base/BoardUpdaterTest.cpp | 104 + src/unittest/libpentobi_base/CMakeLists.txt | 19 + src/unittest/libpentobi_base/GameTest.cpp | 44 + src/unittest/libpentobi_base/TreeTest.cpp | 112 + src/unittest/libpentobi_mcts/CMakeLists.txt | 20 + src/unittest/libpentobi_mcts/SearchTest.cpp | 115 + windows/CMakeLists.txt | 20 + windows/German.nsh | 10 + windows/blksgf.ico | Bin 0 -> 17020 bytes windows/install.nsis.in | 143 + 575 files changed, 59610 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 COPYING create mode 100644 INSTALL create mode 100644 NEWS create mode 100644 README create mode 100644 config.h.in create mode 100644 data/CMakeLists.txt create mode 100644 data/application-x-blokus-sgf-16.svg create mode 100644 data/application-x-blokus-sgf-32.svg create mode 100644 data/application-x-blokus-sgf-64.svg create mode 100644 data/application-x-blokus-sgf.svg create mode 100644 data/pentobi-mime.xml create mode 100644 data/pentobi.appdata.xml.in create mode 100755 data/pentobi.desktop.in create mode 100644 data/pentobi.thumbnailer.in create mode 100644 doc/CMakeLists.txt create mode 100644 doc/blksgf/Pentobi-SGF.html create mode 100644 doc/doxygen/.gitignore create mode 100644 doc/doxygen/Doxyfile create mode 100644 doc/doxygen/footer.html create mode 100644 doc/gtp/Pentobi-GTP.html create mode 100644 doc/man/CMakeLists.txt create mode 100644 doc/man/pentobi-thumbnailer.6.in create mode 100644 doc/man/pentobi.6.in create mode 100644 src/CMakeLists.txt create mode 100644 src/books/CMakeLists.txt create mode 100644 src/books/book_callisto.blksgf create mode 100644 src/books/book_callisto_2.blksgf create mode 100644 src/books/book_callisto_3.blksgf create mode 100644 src/books/book_classic.blksgf create mode 100644 src/books/book_classic_2.blksgf create mode 100644 src/books/book_classic_3.blksgf create mode 100644 src/books/book_duo.blksgf create mode 100644 src/books/book_junior.blksgf create mode 100644 src/books/book_nexos.blksgf create mode 100644 src/books/book_nexos_2.blksgf create mode 100644 src/books/book_trigon.blksgf create mode 100644 src/books/book_trigon_2.blksgf create mode 100644 src/books/book_trigon_3.blksgf create mode 100644 src/books/pentobi_books.qrc create mode 100644 src/convert/CMakeLists.txt create mode 100644 src/convert/Main.cpp create mode 100644 src/doc_libboardgame.cpp create mode 100644 src/doc_mainpage.cpp create mode 100644 src/libboardgame_base/CMakeLists.txt create mode 100644 src/libboardgame_base/CoordPoint.cpp create mode 100644 src/libboardgame_base/CoordPoint.h create mode 100644 src/libboardgame_base/Engine.cpp create mode 100644 src/libboardgame_base/Engine.h create mode 100644 src/libboardgame_base/Geometry.h create mode 100644 src/libboardgame_base/GeometryUtil.h create mode 100644 src/libboardgame_base/Grid.h create mode 100644 src/libboardgame_base/Marker.h create mode 100644 src/libboardgame_base/Point.h create mode 100644 src/libboardgame_base/PointTransform.h create mode 100644 src/libboardgame_base/Rating.cpp create mode 100644 src/libboardgame_base/Rating.h create mode 100644 src/libboardgame_base/RectGeometry.h create mode 100644 src/libboardgame_base/RectTransform.cpp create mode 100644 src/libboardgame_base/RectTransform.h create mode 100644 src/libboardgame_base/StringRep.cpp create mode 100644 src/libboardgame_base/StringRep.h create mode 100644 src/libboardgame_base/Transform.cpp create mode 100644 src/libboardgame_base/Transform.h create mode 100644 src/libboardgame_gtp/Arguments.cpp create mode 100644 src/libboardgame_gtp/Arguments.h create mode 100644 src/libboardgame_gtp/CMakeLists.txt create mode 100644 src/libboardgame_gtp/CmdLine.cpp create mode 100644 src/libboardgame_gtp/CmdLine.h create mode 100644 src/libboardgame_gtp/CmdLineRange.h create mode 100644 src/libboardgame_gtp/Engine.cpp create mode 100644 src/libboardgame_gtp/Engine.h create mode 100644 src/libboardgame_gtp/Failure.h create mode 100644 src/libboardgame_gtp/Response.cpp create mode 100644 src/libboardgame_gtp/Response.h create mode 100644 src/libboardgame_mcts/Atomic.h create mode 100644 src/libboardgame_mcts/CMakeLists.txt create mode 100644 src/libboardgame_mcts/LastGoodReply.h create mode 100644 src/libboardgame_mcts/Node.h create mode 100644 src/libboardgame_mcts/PlayerMove.h create mode 100644 src/libboardgame_mcts/SearchBase.h create mode 100644 src/libboardgame_mcts/Tree.h create mode 100644 src/libboardgame_mcts/TreeUtil.h create mode 100644 src/libboardgame_sgf/CMakeLists.txt create mode 100644 src/libboardgame_sgf/InvalidPropertyValue.h create mode 100644 src/libboardgame_sgf/InvalidTree.h create mode 100644 src/libboardgame_sgf/MissingProperty.cpp create mode 100644 src/libboardgame_sgf/MissingProperty.h create mode 100644 src/libboardgame_sgf/Reader.cpp create mode 100644 src/libboardgame_sgf/Reader.h create mode 100644 src/libboardgame_sgf/SgfNode.cpp create mode 100644 src/libboardgame_sgf/SgfNode.h create mode 100644 src/libboardgame_sgf/SgfTree.cpp create mode 100644 src/libboardgame_sgf/SgfTree.h create mode 100644 src/libboardgame_sgf/SgfUtil.cpp create mode 100644 src/libboardgame_sgf/SgfUtil.h create mode 100644 src/libboardgame_sgf/TreeReader.cpp create mode 100644 src/libboardgame_sgf/TreeReader.h create mode 100644 src/libboardgame_sgf/TreeWriter.cpp create mode 100644 src/libboardgame_sgf/TreeWriter.h create mode 100644 src/libboardgame_sgf/Writer.cpp create mode 100644 src/libboardgame_sgf/Writer.h create mode 100644 src/libboardgame_sys/CMakeLists.txt create mode 100644 src/libboardgame_sys/Compiler.h create mode 100644 src/libboardgame_sys/CpuTime.cpp create mode 100644 src/libboardgame_sys/CpuTime.h create mode 100644 src/libboardgame_sys/Memory.cpp create mode 100644 src/libboardgame_sys/Memory.h create mode 100644 src/libboardgame_test/CMakeLists.txt create mode 100644 src/libboardgame_test/Test.cpp create mode 100644 src/libboardgame_test/Test.h create mode 100644 src/libboardgame_test_main/CMakeLists.txt create mode 100644 src/libboardgame_test_main/Main.cpp create mode 100644 src/libboardgame_util/Abort.cpp create mode 100644 src/libboardgame_util/Abort.h create mode 100644 src/libboardgame_util/ArrayList.h create mode 100644 src/libboardgame_util/Assert.cpp create mode 100644 src/libboardgame_util/Assert.h create mode 100644 src/libboardgame_util/Barrier.cpp create mode 100644 src/libboardgame_util/Barrier.h create mode 100644 src/libboardgame_util/CMakeLists.txt create mode 100644 src/libboardgame_util/CpuTimeSource.cpp create mode 100644 src/libboardgame_util/CpuTimeSource.h create mode 100644 src/libboardgame_util/FmtSaver.h create mode 100644 src/libboardgame_util/IntervalChecker.cpp create mode 100644 src/libboardgame_util/IntervalChecker.h create mode 100644 src/libboardgame_util/Log.cpp create mode 100644 src/libboardgame_util/Log.h create mode 100644 src/libboardgame_util/MathUtil.h create mode 100644 src/libboardgame_util/Options.cpp create mode 100644 src/libboardgame_util/Options.h create mode 100644 src/libboardgame_util/RandomGenerator.cpp create mode 100644 src/libboardgame_util/RandomGenerator.h create mode 100644 src/libboardgame_util/Range.h create mode 100644 src/libboardgame_util/Statistics.h create mode 100644 src/libboardgame_util/StringUtil.cpp create mode 100644 src/libboardgame_util/StringUtil.h create mode 100644 src/libboardgame_util/TimeIntervalChecker.cpp create mode 100644 src/libboardgame_util/TimeIntervalChecker.h create mode 100644 src/libboardgame_util/TimeSource.cpp create mode 100644 src/libboardgame_util/TimeSource.h create mode 100644 src/libboardgame_util/Timer.cpp create mode 100644 src/libboardgame_util/Timer.h create mode 100644 src/libboardgame_util/Unused.h create mode 100644 src/libboardgame_util/WallTimeSource.cpp create mode 100644 src/libboardgame_util/WallTimeSource.h create mode 100644 src/libpentobi_base/Board.cpp create mode 100644 src/libpentobi_base/Board.h create mode 100644 src/libpentobi_base/BoardConst.cpp create mode 100644 src/libpentobi_base/BoardConst.h create mode 100644 src/libpentobi_base/BoardUpdater.cpp create mode 100644 src/libpentobi_base/BoardUpdater.h create mode 100644 src/libpentobi_base/BoardUtil.cpp create mode 100644 src/libpentobi_base/BoardUtil.h create mode 100644 src/libpentobi_base/Book.cpp create mode 100644 src/libpentobi_base/Book.h create mode 100644 src/libpentobi_base/CMakeLists.txt create mode 100644 src/libpentobi_base/CallistoGeometry.cpp create mode 100644 src/libpentobi_base/CallistoGeometry.h create mode 100644 src/libpentobi_base/Color.cpp create mode 100644 src/libpentobi_base/Color.h create mode 100644 src/libpentobi_base/ColorMap.h create mode 100644 src/libpentobi_base/ColorMove.h create mode 100644 src/libpentobi_base/Engine.cpp create mode 100644 src/libpentobi_base/Engine.h create mode 100644 src/libpentobi_base/Game.cpp create mode 100644 src/libpentobi_base/Game.h create mode 100644 src/libpentobi_base/Geometry.h create mode 100644 src/libpentobi_base/Grid.h create mode 100644 src/libpentobi_base/Marker.h create mode 100644 src/libpentobi_base/Move.h create mode 100644 src/libpentobi_base/MoveInfo.h create mode 100644 src/libpentobi_base/MoveList.h create mode 100644 src/libpentobi_base/MoveMarker.h create mode 100644 src/libpentobi_base/MovePoints.h create mode 100644 src/libpentobi_base/NexosGeometry.cpp create mode 100644 src/libpentobi_base/NexosGeometry.h create mode 100644 src/libpentobi_base/NodeUtil.cpp create mode 100644 src/libpentobi_base/NodeUtil.h create mode 100644 src/libpentobi_base/PentobiSgfUtil.cpp create mode 100644 src/libpentobi_base/PentobiSgfUtil.h create mode 100644 src/libpentobi_base/PentobiTree.cpp create mode 100644 src/libpentobi_base/PentobiTree.h create mode 100644 src/libpentobi_base/PentobiTreeWriter.cpp create mode 100644 src/libpentobi_base/PentobiTreeWriter.h create mode 100644 src/libpentobi_base/Piece.h create mode 100644 src/libpentobi_base/PieceInfo.cpp create mode 100644 src/libpentobi_base/PieceInfo.h create mode 100644 src/libpentobi_base/PieceMap.h create mode 100644 src/libpentobi_base/PieceTransforms.cpp create mode 100644 src/libpentobi_base/PieceTransforms.h create mode 100644 src/libpentobi_base/PieceTransformsClassic.cpp create mode 100644 src/libpentobi_base/PieceTransformsClassic.h create mode 100644 src/libpentobi_base/PieceTransformsTrigon.cpp create mode 100644 src/libpentobi_base/PieceTransformsTrigon.h create mode 100644 src/libpentobi_base/PlayerBase.cpp create mode 100644 src/libpentobi_base/PlayerBase.h create mode 100644 src/libpentobi_base/Point.h create mode 100644 src/libpentobi_base/PointList.h create mode 100644 src/libpentobi_base/PointState.cpp create mode 100644 src/libpentobi_base/PointState.h create mode 100644 src/libpentobi_base/PrecompMoves.h create mode 100644 src/libpentobi_base/ScoreUtil.h create mode 100644 src/libpentobi_base/Setup.h create mode 100644 src/libpentobi_base/StartingPoints.cpp create mode 100644 src/libpentobi_base/StartingPoints.h create mode 100644 src/libpentobi_base/SymmetricPoints.cpp create mode 100644 src/libpentobi_base/SymmetricPoints.h create mode 100644 src/libpentobi_base/TreeUtil.cpp create mode 100644 src/libpentobi_base/TreeUtil.h create mode 100644 src/libpentobi_base/TrigonGeometry.cpp create mode 100644 src/libpentobi_base/TrigonGeometry.h create mode 100644 src/libpentobi_base/TrigonTransform.cpp create mode 100644 src/libpentobi_base/TrigonTransform.h create mode 100644 src/libpentobi_base/Variant.cpp create mode 100644 src/libpentobi_base/Variant.h create mode 100644 src/libpentobi_gui/BoardPainter.cpp create mode 100644 src/libpentobi_gui/BoardPainter.h create mode 100644 src/libpentobi_gui/CMakeLists.txt create mode 100644 src/libpentobi_gui/ComputerColorDialog.cpp create mode 100644 src/libpentobi_gui/ComputerColorDialog.h create mode 100644 src/libpentobi_gui/GameInfoDialog.cpp create mode 100644 src/libpentobi_gui/GameInfoDialog.h create mode 100644 src/libpentobi_gui/GuiBoard.cpp create mode 100644 src/libpentobi_gui/GuiBoard.h create mode 100644 src/libpentobi_gui/GuiBoardUtil.cpp create mode 100644 src/libpentobi_gui/GuiBoardUtil.h create mode 100644 src/libpentobi_gui/HelpWindow.cpp create mode 100644 src/libpentobi_gui/HelpWindow.h create mode 100644 src/libpentobi_gui/InitialRatingDialog.cpp create mode 100644 src/libpentobi_gui/InitialRatingDialog.h create mode 100644 src/libpentobi_gui/LeaveFullscreenButton.cpp create mode 100644 src/libpentobi_gui/LeaveFullscreenButton.h create mode 100644 src/libpentobi_gui/LineEdit.cpp create mode 100644 src/libpentobi_gui/LineEdit.h create mode 100644 src/libpentobi_gui/OrientationDisplay.cpp create mode 100644 src/libpentobi_gui/OrientationDisplay.h create mode 100644 src/libpentobi_gui/PieceSelector.cpp create mode 100644 src/libpentobi_gui/PieceSelector.h create mode 100644 src/libpentobi_gui/SameHeightLayout.cpp create mode 100644 src/libpentobi_gui/SameHeightLayout.h create mode 100644 src/libpentobi_gui/ScoreDisplay.cpp create mode 100644 src/libpentobi_gui/ScoreDisplay.h create mode 100644 src/libpentobi_gui/Util.cpp create mode 100644 src/libpentobi_gui/Util.h create mode 100644 src/libpentobi_gui/icons/go-home.svg create mode 100644 src/libpentobi_gui/icons/go-next.svg create mode 100644 src/libpentobi_gui/icons/go-previous.svg create mode 100644 src/libpentobi_gui/libpentobi_gui_resources.qrc create mode 100644 src/libpentobi_gui/libpentobi_gui_resources_2x.qrc create mode 100644 src/libpentobi_gui/translations/libpentobi_gui_de.ts create mode 100644 src/libpentobi_kde_thumbnailer/CMakeLists.txt create mode 100644 src/libpentobi_mcts/AnalyzeGame.cpp create mode 100644 src/libpentobi_mcts/AnalyzeGame.h create mode 100644 src/libpentobi_mcts/CMakeLists.txt create mode 100644 src/libpentobi_mcts/Float.h create mode 100644 src/libpentobi_mcts/History.cpp create mode 100644 src/libpentobi_mcts/History.h create mode 100644 src/libpentobi_mcts/Player.cpp create mode 100644 src/libpentobi_mcts/Player.h create mode 100644 src/libpentobi_mcts/PlayoutFeatures.cpp create mode 100644 src/libpentobi_mcts/PlayoutFeatures.h create mode 100644 src/libpentobi_mcts/PriorKnowledge.cpp create mode 100644 src/libpentobi_mcts/PriorKnowledge.h create mode 100644 src/libpentobi_mcts/Search.cpp create mode 100644 src/libpentobi_mcts/Search.h create mode 100644 src/libpentobi_mcts/SearchParamConst.h create mode 100644 src/libpentobi_mcts/SharedConst.cpp create mode 100644 src/libpentobi_mcts/SharedConst.h create mode 100644 src/libpentobi_mcts/State.cpp create mode 100644 src/libpentobi_mcts/State.h create mode 100644 src/libpentobi_mcts/StateUtil.cpp create mode 100644 src/libpentobi_mcts/StateUtil.h create mode 100644 src/libpentobi_mcts/Util.cpp create mode 100644 src/libpentobi_mcts/Util.h create mode 100644 src/libpentobi_thumbnail/CMakeLists.txt create mode 100644 src/libpentobi_thumbnail/CreateThumbnail.cpp create mode 100644 src/libpentobi_thumbnail/CreateThumbnail.h create mode 100644 src/pentobi/AnalyzeGameWidget.cpp create mode 100644 src/pentobi/AnalyzeGameWidget.h create mode 100644 src/pentobi/AnalyzeGameWindow.cpp create mode 100644 src/pentobi/AnalyzeGameWindow.h create mode 100644 src/pentobi/AnalyzeSpeedDialog.cpp create mode 100644 src/pentobi/AnalyzeSpeedDialog.h create mode 100644 src/pentobi/Application.cpp create mode 100644 src/pentobi/Application.h create mode 100644 src/pentobi/CMakeLists.txt create mode 100644 src/pentobi/Main.cpp create mode 100644 src/pentobi/MainWindow.cpp create mode 100644 src/pentobi/MainWindow.h create mode 100644 src/pentobi/RatedGamesList.cpp create mode 100644 src/pentobi/RatedGamesList.h create mode 100644 src/pentobi/RatingDialog.cpp create mode 100644 src/pentobi/RatingDialog.h create mode 100644 src/pentobi/RatingGraph.cpp create mode 100644 src/pentobi/RatingGraph.h create mode 100644 src/pentobi/RatingHistory.cpp create mode 100644 src/pentobi/RatingHistory.h create mode 100644 src/pentobi/ShowMessage.cpp create mode 100644 src/pentobi/ShowMessage.h create mode 100644 src/pentobi/Util.cpp create mode 100644 src/pentobi/Util.h create mode 100644 src/pentobi/help/C/pentobi/analysis.jpg create mode 100644 src/pentobi/help/C/pentobi/become_stronger.html create mode 100644 src/pentobi/help/C/pentobi/board_callisto.png create mode 100644 src/pentobi/help/C/pentobi/board_classic.png create mode 100644 src/pentobi/help/C/pentobi/board_duo.png create mode 100644 src/pentobi/help/C/pentobi/board_nexos.png create mode 100644 src/pentobi/help/C/pentobi/board_trigon.jpg create mode 100644 src/pentobi/help/C/pentobi/callisto_rules.html create mode 100644 src/pentobi/help/C/pentobi/classic_rules.html create mode 100644 src/pentobi/help/C/pentobi/duo_rules.html create mode 100644 src/pentobi/help/C/pentobi/index.html create mode 100644 src/pentobi/help/C/pentobi/junior_rules.html create mode 100644 src/pentobi/help/C/pentobi/license.html create mode 100644 src/pentobi/help/C/pentobi/nexos_rules.html create mode 100644 src/pentobi/help/C/pentobi/pieces.png create mode 100644 src/pentobi/help/C/pentobi/pieces_callisto.png create mode 100644 src/pentobi/help/C/pentobi/pieces_junior.png create mode 100644 src/pentobi/help/C/pentobi/pieces_nexos.png create mode 100644 src/pentobi/help/C/pentobi/pieces_trigon.jpg create mode 100644 src/pentobi/help/C/pentobi/position_callisto.png create mode 100644 src/pentobi/help/C/pentobi/position_classic.png create mode 100644 src/pentobi/help/C/pentobi/position_duo.png create mode 100644 src/pentobi/help/C/pentobi/position_nexos.png create mode 100644 src/pentobi/help/C/pentobi/position_trigon.jpg create mode 100644 src/pentobi/help/C/pentobi/rating.jpg create mode 100644 src/pentobi/help/C/pentobi/shortcuts.html create mode 100644 src/pentobi/help/C/pentobi/stylesheet.css create mode 100644 src/pentobi/help/C/pentobi/system.html create mode 100644 src/pentobi/help/C/pentobi/trigon_rules.html create mode 100644 src/pentobi/help/C/pentobi/user_interface.html create mode 100644 src/pentobi/help/C/pentobi/window_menu.html create mode 100644 src/pentobi/help/de/pentobi/become_stronger.html create mode 100644 src/pentobi/help/de/pentobi/callisto_rules.html create mode 100644 src/pentobi/help/de/pentobi/classic_rules.html create mode 100644 src/pentobi/help/de/pentobi/duo_rules.html create mode 100644 src/pentobi/help/de/pentobi/index.html create mode 100644 src/pentobi/help/de/pentobi/junior_rules.html create mode 100644 src/pentobi/help/de/pentobi/license.html create mode 100644 src/pentobi/help/de/pentobi/nexos_rules.html create mode 100644 src/pentobi/help/de/pentobi/shortcuts.html create mode 100644 src/pentobi/help/de/pentobi/system.html create mode 100644 src/pentobi/help/de/pentobi/trigon_rules.html create mode 100644 src/pentobi/help/de/pentobi/user_interface.html create mode 100644 src/pentobi/help/de/pentobi/window_menu.html create mode 100644 src/pentobi/icons.qrc create mode 100644 src/pentobi/icons/pentobi-16.svg create mode 100644 src/pentobi/icons/pentobi-32.svg create mode 100644 src/pentobi/icons/pentobi-64.svg create mode 100644 src/pentobi/icons/pentobi-backward-16.svg create mode 100644 src/pentobi/icons/pentobi-backward.svg create mode 100644 src/pentobi/icons/pentobi-beginning-16.svg create mode 100644 src/pentobi/icons/pentobi-beginning.svg create mode 100644 src/pentobi/icons/pentobi-computer-colors-16.svg create mode 100644 src/pentobi/icons/pentobi-computer-colors.svg create mode 100644 src/pentobi/icons/pentobi-end-16.svg create mode 100644 src/pentobi/icons/pentobi-end.svg create mode 100644 src/pentobi/icons/pentobi-flip-horizontal.svg create mode 100644 src/pentobi/icons/pentobi-flip-vertical.svg create mode 100644 src/pentobi/icons/pentobi-forward-16.svg create mode 100644 src/pentobi/icons/pentobi-forward.svg create mode 100644 src/pentobi/icons/pentobi-newgame-16.svg create mode 100644 src/pentobi/icons/pentobi-newgame.svg create mode 100644 src/pentobi/icons/pentobi-next-piece.svg create mode 100644 src/pentobi/icons/pentobi-next-variation-16.svg create mode 100644 src/pentobi/icons/pentobi-next-variation.svg create mode 100644 src/pentobi/icons/pentobi-piece-clear.svg create mode 100644 src/pentobi/icons/pentobi-play-16.svg create mode 100644 src/pentobi/icons/pentobi-play.svg create mode 100644 src/pentobi/icons/pentobi-previous-piece.svg create mode 100644 src/pentobi/icons/pentobi-previous-variation-16.svg create mode 100644 src/pentobi/icons/pentobi-previous-variation.svg create mode 100644 src/pentobi/icons/pentobi-rated-game-16.svg create mode 100644 src/pentobi/icons/pentobi-rated-game.svg create mode 100644 src/pentobi/icons/pentobi-rotate-left.svg create mode 100644 src/pentobi/icons/pentobi-rotate-right.svg create mode 100644 src/pentobi/icons/pentobi-undo-16.svg create mode 100644 src/pentobi/icons/pentobi-undo.svg create mode 100644 src/pentobi/icons/pentobi.svg create mode 100644 src/pentobi/pentobi.conf.in create mode 100644 src/pentobi/pentobi.ico create mode 100644 src/pentobi/pentobi.rc create mode 100644 src/pentobi/resources.qrc create mode 100644 src/pentobi/resources_2x.qrc create mode 100644 src/pentobi/translations/pentobi.ts create mode 100644 src/pentobi/translations/pentobi_de.ts create mode 100644 src/pentobi_gtp/CMakeLists.txt create mode 100644 src/pentobi_gtp/Engine.cpp create mode 100644 src/pentobi_gtp/Engine.h create mode 100644 src/pentobi_gtp/Main.cpp create mode 100644 src/pentobi_kde_thumbnailer/CMakeLists.txt create mode 100644 src/pentobi_kde_thumbnailer/PentobiThumbCreator.cpp create mode 100644 src/pentobi_kde_thumbnailer/PentobiThumbCreator.h create mode 100644 src/pentobi_kde_thumbnailer/pentobi-thumbnail.desktop create mode 100644 src/pentobi_qml/.gitignore create mode 100644 src/pentobi_qml/CMakeLists.txt create mode 100644 src/pentobi_qml/GameModel.cpp create mode 100644 src/pentobi_qml/GameModel.h create mode 100644 src/pentobi_qml/Main.cpp create mode 100644 src/pentobi_qml/Pentobi.pro create mode 100644 src/pentobi_qml/PieceModel.cpp create mode 100644 src/pentobi_qml/PieceModel.h create mode 100644 src/pentobi_qml/PlayerModel.cpp create mode 100644 src/pentobi_qml/PlayerModel.h create mode 100644 src/pentobi_qml/android/AndroidManifest.xml create mode 100644 src/pentobi_qml/android/res/drawable-hdpi/icon.png create mode 100644 src/pentobi_qml/android/res/drawable-mdpi/icon.png create mode 100644 src/pentobi_qml/android/res/drawable/splash.xml create mode 100644 src/pentobi_qml/android/res/values/theme.xml create mode 100644 src/pentobi_qml/android_icons_svg/icon48.svg create mode 100644 src/pentobi_qml/android_icons_svg/icon72.svg create mode 100644 src/pentobi_qml/deployment.pri create mode 100644 src/pentobi_qml/icons_android.qrc create mode 100644 src/pentobi_qml/qml/.gitignore create mode 100644 src/pentobi_qml/qml/AndroidToolBar.qml create mode 100644 src/pentobi_qml/qml/AndroidToolButton.qml create mode 100644 src/pentobi_qml/qml/Board.qml create mode 100644 src/pentobi_qml/qml/Button.qml create mode 100644 src/pentobi_qml/qml/ComputerColorDialog.qml create mode 100644 src/pentobi_qml/qml/GameDisplay.js create mode 100644 src/pentobi_qml/qml/GameDisplay.qml create mode 100644 src/pentobi_qml/qml/LineSegment.qml create mode 100644 src/pentobi_qml/qml/Main.js create mode 100644 src/pentobi_qml/qml/Main.qml create mode 100644 src/pentobi_qml/qml/MenuComputer.qml create mode 100644 src/pentobi_qml/qml/MenuEdit.qml create mode 100644 src/pentobi_qml/qml/MenuGame.qml create mode 100644 src/pentobi_qml/qml/MenuGo.qml create mode 100644 src/pentobi_qml/qml/MenuHelp.qml create mode 100644 src/pentobi_qml/qml/MenuItemGameVariant.qml create mode 100644 src/pentobi_qml/qml/MenuItemLevel.qml create mode 100644 src/pentobi_qml/qml/MenuView.qml create mode 100644 src/pentobi_qml/qml/NavigationPanel.qml create mode 100644 src/pentobi_qml/qml/OpenDialog.qml create mode 100644 src/pentobi_qml/qml/PieceCallisto.qml create mode 100644 src/pentobi_qml/qml/PieceClassic.qml create mode 100644 src/pentobi_qml/qml/PieceFlipAnimation.qml create mode 100644 src/pentobi_qml/qml/PieceList.qml create mode 100644 src/pentobi_qml/qml/PieceManipulator.qml create mode 100644 src/pentobi_qml/qml/PieceNexos.qml create mode 100644 src/pentobi_qml/qml/PieceRotationAnimation.qml create mode 100644 src/pentobi_qml/qml/PieceSelector.qml create mode 100644 src/pentobi_qml/qml/PieceTrigon.qml create mode 100644 src/pentobi_qml/qml/SaveDialog.qml create mode 100644 src/pentobi_qml/qml/ScoreDisplay.qml create mode 100644 src/pentobi_qml/qml/ScoreElement.qml create mode 100644 src/pentobi_qml/qml/ScoreElement2.qml create mode 100644 src/pentobi_qml/qml/Square.qml create mode 100644 src/pentobi_qml/qml/ToolBar.qml create mode 100644 src/pentobi_qml/qml/Triangle.qml create mode 100644 src/pentobi_qml/qml/i18n/qml_de.ts create mode 100644 src/pentobi_qml/qml/i18n/replace_qtbase_de.ts create mode 100644 src/pentobi_qml/qml/icons/menu.svg create mode 100644 src/pentobi_qml/qml/icons/pentobi-backward.svg create mode 100644 src/pentobi_qml/qml/icons/pentobi-beginning.svg create mode 100644 src/pentobi_qml/qml/icons/pentobi-computer-colors.svg create mode 100644 src/pentobi_qml/qml/icons/pentobi-end.svg create mode 100644 src/pentobi_qml/qml/icons/pentobi-forward.svg create mode 100644 src/pentobi_qml/qml/icons/pentobi-newgame.svg create mode 100644 src/pentobi_qml/qml/icons/pentobi-next-variation.svg create mode 100644 src/pentobi_qml/qml/icons/pentobi-play.svg create mode 100644 src/pentobi_qml/qml/icons/pentobi-previous-variation.svg create mode 100644 src/pentobi_qml/qml/icons/pentobi-undo.svg create mode 100644 src/pentobi_qml/qml/themes/dark/Theme.qml create mode 100644 src/pentobi_qml/qml/themes/dark/board-callisto-2.svg create mode 100644 src/pentobi_qml/qml/themes/dark/board-callisto-3.svg create mode 100644 src/pentobi_qml/qml/themes/dark/board-callisto.svg create mode 100644 src/pentobi_qml/qml/themes/dark/board-tile-classic.svg create mode 100644 src/pentobi_qml/qml/themes/dark/board-tile-nexos.svg create mode 100644 src/pentobi_qml/qml/themes/dark/board-trigon-3.svg create mode 100644 src/pentobi_qml/qml/themes/dark/board-trigon.svg create mode 100644 src/pentobi_qml/qml/themes/light/Theme.qml create mode 100644 src/pentobi_qml/qml/themes/light/board-callisto-2.svg create mode 100644 src/pentobi_qml/qml/themes/light/board-callisto-3.svg create mode 100644 src/pentobi_qml/qml/themes/light/board-callisto.svg create mode 100644 src/pentobi_qml/qml/themes/light/board-tile-classic.svg create mode 100644 src/pentobi_qml/qml/themes/light/board-tile-nexos.svg create mode 100644 src/pentobi_qml/qml/themes/light/board-trigon-3.svg create mode 100644 src/pentobi_qml/qml/themes/light/board-trigon.svg create mode 100644 src/pentobi_qml/qml/themes/light/frame-blue.svg create mode 100644 src/pentobi_qml/qml/themes/light/frame-green.svg create mode 100644 src/pentobi_qml/qml/themes/light/frame-red.svg create mode 100644 src/pentobi_qml/qml/themes/light/frame-yellow.svg create mode 100644 src/pentobi_qml/qml/themes/light/junction-all-blue.svg create mode 100644 src/pentobi_qml/qml/themes/light/junction-all-green.svg create mode 100644 src/pentobi_qml/qml/themes/light/junction-all-red.svg create mode 100644 src/pentobi_qml/qml/themes/light/junction-all-yellow.svg create mode 100644 src/pentobi_qml/qml/themes/light/junction-rect-blue.svg create mode 100644 src/pentobi_qml/qml/themes/light/junction-rect-green.svg create mode 100644 src/pentobi_qml/qml/themes/light/junction-rect-red.svg create mode 100644 src/pentobi_qml/qml/themes/light/junction-rect-yellow.svg create mode 100644 src/pentobi_qml/qml/themes/light/junction-straight-blue.svg create mode 100644 src/pentobi_qml/qml/themes/light/junction-straight-green.svg create mode 100644 src/pentobi_qml/qml/themes/light/junction-straight-red.svg create mode 100644 src/pentobi_qml/qml/themes/light/junction-straight-yellow.svg create mode 100644 src/pentobi_qml/qml/themes/light/junction-t-blue.svg create mode 100644 src/pentobi_qml/qml/themes/light/junction-t-green.svg create mode 100644 src/pentobi_qml/qml/themes/light/junction-t-red.svg create mode 100644 src/pentobi_qml/qml/themes/light/junction-t-yellow.svg create mode 100644 src/pentobi_qml/qml/themes/light/linesegment-blue.svg create mode 100644 src/pentobi_qml/qml/themes/light/linesegment-green.svg create mode 100644 src/pentobi_qml/qml/themes/light/linesegment-red.svg create mode 100644 src/pentobi_qml/qml/themes/light/linesegment-yellow.svg create mode 100644 src/pentobi_qml/qml/themes/light/piece-manipulator-legal.svg create mode 100644 src/pentobi_qml/qml/themes/light/piece-manipulator.svg create mode 100644 src/pentobi_qml/qml/themes/light/square-blue.svg create mode 100644 src/pentobi_qml/qml/themes/light/square-green.svg create mode 100644 src/pentobi_qml/qml/themes/light/square-red.svg create mode 100644 src/pentobi_qml/qml/themes/light/square-yellow.svg create mode 100644 src/pentobi_qml/qml/themes/light/triangle-blue.svg create mode 100644 src/pentobi_qml/qml/themes/light/triangle-down-blue.svg create mode 100644 src/pentobi_qml/qml/themes/light/triangle-down-green.svg create mode 100644 src/pentobi_qml/qml/themes/light/triangle-down-red.svg create mode 100644 src/pentobi_qml/qml/themes/light/triangle-down-yellow.svg create mode 100644 src/pentobi_qml/qml/themes/light/triangle-green.svg create mode 100644 src/pentobi_qml/qml/themes/light/triangle-red.svg create mode 100644 src/pentobi_qml/qml/themes/light/triangle-yellow.svg create mode 100644 src/pentobi_qml/qml/themes/theme_dark.qrc create mode 100644 src/pentobi_qml/qml/themes/theme_light.qrc create mode 100644 src/pentobi_qml/qml/themes/theme_shared.qrc create mode 100644 src/pentobi_qml/resources.qrc create mode 100644 src/pentobi_qml/translations.qrc create mode 100644 src/pentobi_thumbnailer/CMakeLists.txt create mode 100644 src/pentobi_thumbnailer/Main.cpp create mode 100644 src/twogtp/Analyze.cpp create mode 100644 src/twogtp/Analyze.h create mode 100644 src/twogtp/CMakeLists.txt create mode 100644 src/twogtp/FdStream.cpp create mode 100644 src/twogtp/FdStream.h create mode 100644 src/twogtp/GtpConnection.cpp create mode 100644 src/twogtp/GtpConnection.h create mode 100644 src/twogtp/Main.cpp create mode 100644 src/twogtp/Output.cpp create mode 100644 src/twogtp/Output.h create mode 100644 src/twogtp/OutputTree.cpp create mode 100644 src/twogtp/OutputTree.h create mode 100644 src/twogtp/TwoGtp.cpp create mode 100644 src/twogtp/TwoGtp.h create mode 100644 src/unittest/CMakeLists.txt create mode 100644 src/unittest/libboardgame_base/CMakeLists.txt create mode 100644 src/unittest/libboardgame_base/MarkerTest.cpp create mode 100644 src/unittest/libboardgame_base/PointTransformTest.cpp create mode 100644 src/unittest/libboardgame_base/RatingTest.cpp create mode 100644 src/unittest/libboardgame_base/RectGeometryTest.cpp create mode 100644 src/unittest/libboardgame_base/StringRepTest.cpp create mode 100644 src/unittest/libboardgame_gtp/ArgumentsTest.cpp create mode 100644 src/unittest/libboardgame_gtp/CMakeLists.txt create mode 100644 src/unittest/libboardgame_gtp/CmdLineTest.cpp create mode 100644 src/unittest/libboardgame_gtp/EngineTest.cpp create mode 100644 src/unittest/libboardgame_gtp/ResponseTest.cpp create mode 100644 src/unittest/libboardgame_mcts/CMakeLists.txt create mode 100644 src/unittest/libboardgame_mcts/NodeTest.cpp create mode 100644 src/unittest/libboardgame_sgf/CMakeLists.txt create mode 100644 src/unittest/libboardgame_sgf/SgfNodeTest.cpp create mode 100644 src/unittest/libboardgame_sgf/SgfUtilTest.cpp create mode 100644 src/unittest/libboardgame_sgf/TreeReaderTest.cpp create mode 100644 src/unittest/libboardgame_util/ArrayListTest.cpp create mode 100644 src/unittest/libboardgame_util/CMakeLists.txt create mode 100644 src/unittest/libboardgame_util/OptionsTest.cpp create mode 100644 src/unittest/libboardgame_util/StatisticsTest.cpp create mode 100644 src/unittest/libboardgame_util/StringUtilTest.cpp create mode 100644 src/unittest/libpentobi_base/BoardConstTest.cpp create mode 100644 src/unittest/libpentobi_base/BoardTest.cpp create mode 100644 src/unittest/libpentobi_base/BoardUpdaterTest.cpp create mode 100644 src/unittest/libpentobi_base/CMakeLists.txt create mode 100644 src/unittest/libpentobi_base/GameTest.cpp create mode 100644 src/unittest/libpentobi_base/TreeTest.cpp create mode 100644 src/unittest/libpentobi_mcts/CMakeLists.txt create mode 100644 src/unittest/libpentobi_mcts/SearchTest.cpp create mode 100644 windows/CMakeLists.txt create mode 100644 windows/German.nsh create mode 100644 windows/blksgf.ico create mode 100644 windows/install.nsis.in diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2b21e9e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +CMakeLists.txt.user +Pentobi.creator.user diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..ff848ce --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,106 @@ +cmake_minimum_required(VERSION 3.0.2) + +project(Pentobi) +set(PENTOBI_VERSION 12.2) +set(PENTOBI_RELEASE_DATE 2017-01-05) + +cmake_policy(SET CMP0043 NEW) + +include(CheckIncludeFiles) +include(GNUInstallDirs) + +option(PENTOBI_BUILD_TESTS "Build unit tests" OFF) +option(PENTOBI_BUILD_GTP "Build GTP interface" OFF) +option(PENTOBI_BUILD_GUI "Build Qt-based GUI" ON) +option(PENTOBI_BUILD_QML "Build QtQuick-based GUI" OFF) +option(PENTOBI_BUILD_KDE_THUMBNAILER "Build thumbnailer for KDE" OFF) + +if (PENTOBI_BUILD_KDE_THUMBNAILER AND NOT PENTOBI_BUILD_GUI) + message(FATAL_ERROR + "PENTOBI_BUILD_KDE_THUMBNAILER requires PENTOBI_BUILD_GUI=1") +endif() + +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + message(STATUS "No build type selected, default to Release") + set(CMAKE_BUILD_TYPE "Release") +endif() +set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DLIBBOARDGAME_DEBUG") + +if(CMAKE_COMPILER_IS_GNUCXX OR (CMAKE_CXX_COMPILER_ID MATCHES "Clang") + OR (CMAKE_CXX_COMPILER_ID MATCHES "Intel")) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") +endif() +if(CMAKE_COMPILER_IS_GNUCXX OR (CMAKE_CXX_COMPILER_ID MATCHES "Clang")) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -ffast-math") +endif() +if(MSVC) + add_definitions(-D_CRT_SECURE_NO_DEPRECATE -D_CRT_NONSTDC_NO_DEPRECATE + -D_SCL_SECURE_NO_WARNINGS) +endif() + +check_include_files(unistd.h HAVE_UNISTD_H) +check_include_files(sys/times.h HAVE_SYS_TIMES_H) +check_include_files(sys/sysctl.h HAVE_SYS_SYSCTL_H) + +if(NOT DEFINED LIBPENTOBI_MCTS_FLOAT_TYPE) + set(LIBPENTOBI_MCTS_FLOAT_TYPE float) +endif() + +# Don't set the Pentobi data dirs on Windows. This is currently needed for +# building a version of Pentobi for the NSIS installer on Windows (see +# directory windows) such that Pentobi will look for data dirs relative to +# the installation directory. (It breaks installing Pentobi on Windows with +# "make install" but we don't support that on Windows anyway.) +if(UNIX) + if(NOT DEFINED PENTOBI_BOOKS_DIR) + set(PENTOBI_BOOKS_DIR "${CMAKE_INSTALL_FULL_DATADIR}/pentobi/books") + endif() + if(NOT DEFINED PENTOBI_HELP_DIR) + set(PENTOBI_HELP_DIR "${CMAKE_INSTALL_FULL_DATAROOTDIR}/help") + endif() + if(NOT DEFINED PENTOBI_TRANSLATIONS) + set(PENTOBI_TRANSLATIONS + "${CMAKE_INSTALL_FULL_DATADIR}/pentobi/translations") + endif() +endif(UNIX) + +configure_file(config.h.in config.h) +add_definitions(-DHAVE_CONFIG_H) +include_directories(${CMAKE_CURRENT_BINARY_DIR}) + +include_directories(${CMAKE_SOURCE_DIR}/src) + +if(PENTOBI_BUILD_TESTS) + enable_testing() +endif() + +find_package(Threads) + +if(PENTOBI_BUILD_GUI) + find_package(Qt5Concurrent 5.2 REQUIRED) + find_package(Qt5Widgets 5.2 REQUIRED) + find_package(Qt5LinguistTools 5.2 REQUIRED) + find_package(Qt5Svg 5.2 REQUIRED) +endif() +if(PENTOBI_BUILD_QML) + # Qt 5.3 is good enough for building but the QML files require Qt 5.6 to run + find_package(Qt5Concurrent 5.3 REQUIRED) + find_package(Qt5Qml 5.3 REQUIRED) + find_package(Qt5Gui 5.3 REQUIRED) + find_package(Qt5Svg 5.3 REQUIRED) +endif() + +if(UNIX) + add_custom_target(dist + COMMAND git archive --prefix=pentobi-${PENTOBI_VERSION}/ HEAD + | xz -e > ${CMAKE_BINARY_DIR}/pentobi-${PENTOBI_VERSION}.tar.xz + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) +endif() + +add_subdirectory(doc) +add_subdirectory(src) +add_subdirectory(data) +if(WIN32 AND PENTOBI_BUILD_GUI) + add_subdirectory(windows) +endif() + diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..72591e1 --- /dev/null +++ b/COPYING @@ -0,0 +1,694 @@ +Copyright (C) 2011-2017 Markus Enzenberger + +Pentobi is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free +Software Foundation, either version 3 of the License, or (at your option) +any later version. + +Pentobi is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +more details. + +A copy of the GNU General Public License version 3 is appended below. + +Trademark disclaimer: The trademark Blokus and other trademarks referred +to are property of their respective trademark holders. The trademark +holders are not affiliated with the author of the program Pentobi. + +-------------------------------------------------------------------------- + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..e60f9fc --- /dev/null +++ b/INSTALL @@ -0,0 +1,60 @@ +This file explains how to compile and install Pentobi from the sources. + + +== Requirements == + +Pentobi requires the Qt libraries (>=5.2). The C++ compiler needs to support +certain C++11 features (only features that are already implemented by +GCC 4.9 and MSVC 2015). The build system uses CMake (>=3.0.2). + +Ubuntu 16.04 provides suitable versions of the required tools and libraries in +its package repository. They can be installed with the shell command: + + sudo apt-get install \ + g++ make cmake qttools5-dev qttools5-dev-tools libqt5svg5-dev + + +== Building == + +Pentobi can be compiled from the source directory with the shell commands: + + cmake -DCMAKE_BUILD_TYPE=Release . + make + + +=== Building the KDE thumbnailer plugin === + +A thumbnailer plugin for KDE can be built by using the cmake option +-DPENTOBI_BUILD_KDE_THUMBNAILER=1. In this case, the KDE development files +need to be installed (packages kio-dev and extra-cmake-modules on +Ubuntu 16.04). Note that on Ubuntu 16.04, the plugin will not be found if +the default installation prefix /usr/local is used. You need to add +QT_PLUGIN_PATH=/usr/local/lib/plugins to /etc/environment. After that, you +can enable previews for Blokus game file in the Dolphin file manager in +"Configure Dolphin/General/Previews". + + +== Installing == + +On Linux, Pentobi can be installed after compilation with the shell command: + + sudo make install + +After installation, the system-wide databases should be updated to +make Pentobi appear in the desktop menu and register it as handler for Blokus +files (*.blksgf). On Ubuntu 16.04 with install prefix /usr/local, this can be +done by running: + + sudo update-mime-database /usr/local/share/mime + sudo update-desktop-database /usr/local/share/applications + + +== Building the Android version == + +For building the Android app, there is a QtCreator project file in +src/pentobi_qml/Pentobi.pro. It requires Qt 5.6. Before compilation, the +binary translation files need to be generated by using File/Release in +Qt Linguist for all TS files in src/pentobi_qml/qml/translations. + +For testing purposes, the GUI that is used for Android can also be built as a +desktop application by running CMake with -DPENTOBI_BUILD_QML=1. diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..4667f33 --- /dev/null +++ b/NEWS @@ -0,0 +1,582 @@ +Version 12.2 (05 Jan 2017) +========================== + +General: + +* Added patterns for Nexos and Callisto SGF files to MIME type + specification for detecting them independent of the file ending. + +Desktop version: + +* Game info properties were not removed from file if the corresponding + text in the game info dialog was deleted. +* New Game/Save As was not enabled if no move had been played but game + was modified by editing the comment in the root node or the game info. +* Fixed a race condition that could cause a crash when updating the + analysis window while a game analysis was running. +* Game analysis progress dialog was not closed if analysis was canceled. + +Android version: + +* Toolbuttons were too small on very high DPI devices. +* Open/Save did not show error message on failure. + + +Version 12.1 (30 Nov 2016) +========================== + +General: + +* Loading a file with a setup position in Nexos did not always work correctly + or could cause a crash. +* SGF files for two-player Callisto did not use B/W properties as documented + but 1/2 as in multi-player variants. Files written by Pentobi 12.0 can still + be read and will be converted if saved again. + +Desktop version: + +* Compilation on Windows is no longer tested or supported. +* Keep Only Position and Keep Only Subtree did not work correctly in Nexos and + in multi-player Callisto. +* Delete All Variations did not mark the file as modified. +* Missing semicolon in desktop entry file (bug #12). +* Fixed ambiguous shortcut overload. +* Saving a file will now remember the directory and use it as a default for + file dialogs. + + +Version 12.0 (10 Apr 2016) +========================== + +General: + +* New game variant Callisto. +* Thinking time of level 7 (the highest level supported on Android) was + increased in most game variants to better match the CPU speed of + typical mobile hardware. +* Starting points are no longer shown after color played its first piece. + +Desktop version: + +* The compilation now requires at least Qt 5.2. +* High-DPI scaling is now automatically used if compiled with Qt 5.6. +* Setting Move Marking to Last now only marks the last move even if the + computer played several moves in a row. + +Bug fixes Desktop version: + +* Icon for undo did not have a high-DPI version. +* Option --verbose was broken on Windows. + +Android version: + +* The compilation now requires Qt 5.6. +* Support for game variant Nexos. +* New menu items Edit/Delete All Variations, Edit/Next Color, + View/Animate Pieces, Help/About. +* Actions with buttons in action bar are no longer shown in menu. +* Forward/backward buttons now support autorepeat. + +Bug fixes Android version: + +* Fixed crash that could occur when switching game variants while a + piece was selected. +* Level set for game variant Classic3 was ignored, instead the level set + for Classic was used. +* Move generation was not properly aborted if some Edit menu items were + selected while the computer was thinking. + + +Version 11.0 (29 Dec 2015) +========================== + +General: + +* Slightly increased playing strength, mainly in Trigon. +* The compilation requires now at least Qt 5.1 and GCC 4.9 or MSVC 2015. +* The score display now shows stars at scores that contain bonuses. + +Desktop version: + +* New game variant Nexos (2 or 4 players). +* If a piece is removed from the board in setup mode, it will now + become the selected piece. +* The command line option --memory was replaced by --maxlevel, which + reduces the needed memory and removes higher levels from the menu. +* The memory requirements are now 1 GB minimum, 4 GB recommended for + playing level 9. +* Added an application metadata file on Linux according to the AppStream + specification from freedesktop.org. Added a 64x64 app icon but no + longer an xpm icon (Debian AppStream Guidelines). + +Bug fixes desktop version: + +* Message dialog about discarding unsaved current game was not shown if + a file was loaded by clicking on a game in the rating dialog. +* Last move marking did not work anymore after after interrupting a + computer move generation and then using Undo Move. +* Autosaving unfinished games did not work if game was finished + first but then made unfinished again with Undo Move. +* Selecting pieces in setup mode did no longer work if no legal moves + were left, even if setup mode is also intended to be used for + setting up illegal positions (e.g. for Blokus art). + +Android version: + +* Initial support for loading/saving, variations and game tree navigation. +* The piece area now has enough room for all pieces of one color. It also + removes rows that become empty and orders the colors such that the color + to play is always on top. +* Action buttons and menu items are now only shown if the action is + enabled in the current position. + + +Version 10.1 (15 Oct 2015) +========================== + +Desktop version: + +* New toolbar button for Undo Move. +* Annotations are now also appended to the move number in the status line. +* Don't show move number in status line if no moves have been played. +* Show an error message instead of the crash dialog if the startup + fails due to low memory. +* The Windows installer is now built with Qt 5 and dynamic libraries. + +Android version: + +* New action bar button for Undo Move. +* Reduced memory requirements. A meaningful error message is now shown + if the startup fails due to low memory. +* Workaround for a bug that made the back button no longer exit the app + after the computer color dialog was shown (QTBUG-48456). +* Faster startup. +* Changed snapping behavior of the piece area to make it easier to flick + vertically between colors with multiple movements on small screens. + + +Version 10.0 (01 Jul 2015) +========================== + +* Increased playing strength and more opening variety in Trigon. +* The Backward10/Forward10 toolbar buttons were replaced by autorepeat + functionality of the Backward/Forward buttons. +* The last move is now by default marked with a dot instead of a number. +* The compilation now requires at least GCC 4.8 and CMake 3.0.2. +* On Linux, the manual is now installed in $PREFIX/share/help according + to the freedesktop.org help specification. +* The KDE thumbnailer plugin can now be compiled with KDE Frameworks 5. +* Better support for high resolution displays if compiled with Qt 5.1 + or newer and environment variable QT_DEVICE_PIXEL_RATIO is used. +* The Pentobi help browser now uses a larger font on Windows +* Regional language subvariants en_GB, en_CA are no longer supported. + +Bug fixes: + +* Fixed a build failure when generating the PNG icons from the SVG sources + if the path contained non-ASCII characters. +* Fixed failure to open a file given as a command line argument to pentobi + (including the case when Pentobi is used as a handler for blksgf files + in file browsers) if the path contained non-ASCII characters. +* Changed the file dialog filter for "All files" from *.* to * such that + really all files are shown even if they have no file ending. + Added an "All files" filter to the Export/ASCII Art file dialog. +* Remembering the playing level separately for each game variant did not + work if the game variant was implicitly changed by opening a file. +* "View/Move Numbers/Last" did not behave correctly after all colors were + enabled in the Computer Colors dialog while a move generation was running. +* Fixed build failure with MSVC if MinGW was not also installed (because + windres.exe was used) + + +Version 9.0 (10 Dec 2014) +========================= + +* Newly supported game variant Classic for 3 players, in which the + players take turns playing the fourth color. +* Increased playing strength, mainly in game variant Trigon. +* There are now 9 levels and the playing strength increases more evenly + with the level. Ratings in rated games are still comparable to previous + versions of Pentobi apart from Trigon at lower levels because Trigon + starts now with a higher playing strength at level 1. +* The computer is now better at playing moves that maximize the score + as long as they do not lead into riskier positions. +* The computer now remembers the playing level separately for each game + variant and restores it when the game variant is changed. +* Player ratings now change faster if less than 30 rated games have been + played, and slower afterwards. +* The mouse wheel can no longer be used for game navigation because it + was too easy to trigger accidentally while playing a game. This also + fixes the bug that the game navigation with the mouse wheel was not + disabled in rated games and the game could not be continued after that + because the play button is disabled in rated games. +* It is no longer possible to select and play a piece while the computer + is thinking, the thinking must be aborted first with Computer/Stop. +* Bugfix: program crashed if computer colors dialog was opened and closed + with OK while computer was thinking. +* Experimental support for Android. The Android version supports only a + subset of the features of the desktop version and only playing levels + 1 to 7. There are still known issues with the user interface due to + bugs in Qt for Android. The Android version is currently only available + as an APK file for devices with an ARMv7 CPU from the download section + of http://pentobi.sourceforge.net + + +Version 8.2 (05 Sep 2014) +========================= + +* Fixed remaining link errors on some platforms (Debian bug #759852) + + +Version 8.1 (31 Aug 2014) +========================= + +* Fixed link error on some platforms if Pentobi is compiled with + PENTOBI_BUILD_TESTS (Debian bug #759852) +* Slightly improved some icons and use icons from theme for more menu items + + +Version 8.0 (02 Mar 2014) +========================= + +* Increased playing strength, especially in game variant Trigon. +* Improved performance on multi-core CPUs: Previously, the move + generation was faster on multi-core CPUs but there was a small drop + in playing strength compared to the same playing level on a + single-core CPU. This effect has been reduced. +* New toolbar button for starting a rated game. +* The interface is now more locked down during rated games, for example + it is no longer possible to change the computer colors or take back a + move during a rated game. +* The menu item "Computer Colors" was moved from the Game to the + Computer menu. +* The source code no longer compiles with MSVC 2012 but requires + MSVC 2013 because a larger subset of C++11 features is used. +* The source code distribution now uses xz instead of gzip for + compression. +* The PNG versions of the icons are no longer included in the source + code but generated at build time from the SVG icons by a small + Qt-based helper program. This adds a build time dependency on QtSvg. +* A XPM icon is now installed to share/pixmaps. +* The configure option USE_BOOST_THREAD is no longer supported. + For building with MinGW, a version of MinGW with support for + std::thread is now required (e.g. from mingwbuilds.sf.net). + + +Version 7.2 (30 Jan 2014) +========================= + +* Hyphens used as minus signs in manpage (bug #9) +* Added keywords section to desktop entry to silence lintian + warning (bug #10) +* Fixed a compilation error with GCC 4.8.2 on PowerPC (and other + big-endian systems) +* Fixed wrong arguments to update-mime-database/update-desktop-database + when running "make post-install" +* Improved a blurry menu item icon +* Fixed a compilation warning about a missing translation +* Reduced the sizes of the generated and installed translation files. +* Fixed a compilation error on 64-bit Linux with X32 ABI +* Fixed a compilation error with Cygwin + + +Version 7.1 (13 Aug 2013) +========================= + +* Fixed the version string. The released file pentobi-7.0.tar.gz was + erroneously built from git version c5247c56 just before the version + tagged with v7.0 and contained the version string 6.UNKNOWN +* The color played by the human in rated games is now randomly assigned +* The mouse wheel is now disabled while the computer is thinking + + +Version 7.0 (25 Jun 2013) +========================= + +* Support for compilation with version 5 of the Qt libraries (see INSTALL + for details) +* Slightly increased playing strength at higher levels (mainly in game + variant Duo) +* The default settings in game variants with more than two players are now + that the human plays the first color and the computer all other colors +* Fixed a crash that could occur if the window was put in fullscreen mode + by a method of the window manager (e.g. title bar menu on KDE) and then + returned to normal mode by a different method (e.g. pressing Escape) + + +Version 6.0 (4 Mar 2013) +======================== + +* Increased playing strength at higher levels. The search algorithm used + for move generation is now parallelized and can take advantage of + multi-core CPUs (up to 4 cores). There is a new playing level 8, which + has a 2 GHz dual-core CPU or faster as the recommended system requirement. +* New menu item Toolbar Text to configure the toolbar button appearance + independent of the system settings +* More SGF game info properties (event, round, time) were added to the + game info dialog +* The source code now requires at least GCC 4.7 (because a larger subset + of C++11 features is used) +* The CMake module GNUInstallDirs is now used for setting the installation + directories on Unix. Note that the defaults for bindir and datadir are + now CMAKE_INSTALL_PREFIX/bin and CMAKE_INSTALL_PREFIX/share instead of + CMAKE_INSTALL_PREFIX/games and CMAKE_INSTALL_PREFIX/share/games. + They can be changed by setting CMAKE_INSTALL_BINDIR and + CMAKE_INSTALL_DATADIR (bug #7) +* The source code no longer depends on the Boost libraries. However, it + is still possible to use Boost.Thread instead of std::thread by + configuring with USE_BOOST_THREAD=ON (e.g. needed on MinGW GCC 4.7, + which has no functional implementation of std::thread) +* Thumbnailer registration for blksgf files is no longer supported for + Gnome 2 + + +Version 5.0 (10 Dec 2012) +========================= + +* Small increase in overall playing strength at higher levels in all game + variants (especially Trigon) +* The computer now knows about the possibility of rotational-symmetric tied + games in game variant Trigon Two-Player (like it already knew in the + variants Duo and Junior) and will prevent the second player from enforcing + such a tie +* If the move generation takes longer than 10 seconds, the maximum remaining + time is now shown in the status bar +* Removed less frequently used buttons (Open, Save) from the tool bar +* Re-organized menu bar +* The menu bar and tool bar are no longer shown in fullscreen mode +* Avoided some window flickering at startup + + +Version 4.3 (2 Nov 2012) +======================== + +* Setting the computer color for Red with the computer colors dialog did + not work for game variant Trigon Three-Player +* Disable Undo menu item when it is not applicable +* Fixed an assertion at end of move generation in Trigon Three-Player if + Pentobi was compiled in debug mode + + +Version 4.2 (7 Oct 2012) +======================== + +* Fixed crash when opening game info dialog in game variants Classic + Two-Player or Trigon Two-Player + + +Version 4.1 (5 Oct 2012) +======================== + +* Result of rated game was counted wrongly in four-color/two-player game + variants if the first player had a higher score than the second player + but the first color a lower score than the second color. +* Fixed potential crash if Undo, Truncate or Truncate Children is selected + while the computer is thinking. +* Automatic continuing of computer play did not work in some cases if the + computer was thinking while the Computer Color dialog was used. + + +Version 4.0 (4 Oct 2012) +======================== + +* New menu item "Beginning of Branch" +* The rating dialog now also shows the best previous rating and has + a button to reset the rating +* A thumbnail plugin for KDE can be built by using the CMake option + -DPENTOBI_BUILD_KDE_THUMBNAILER=ON +* Replaced the icons with less colorful ones. All icons are now licensed + under the GPLv3+ and include SVG sources. No icons from the Tango icon + set are used anymore. + + +Version 3.1 (2 Aug 2012) +======================== + +* Fixed a bug in version 3.0 in the replacement of obsolete move properties + in old files that corrupted files in game variants with 3 or 4 colors. + + +Version 3.0 (1 Aug 2012) +======================== + +* New functionality to compute a player rating for the user by playing + rated games against the computer +* Different options for speed of game analysis +* New menu item "Play Single Move" to make the computer play a move + without changing the colors played by the computer +* The mouse wheel can now be used to navigate in the current variation + if no piece is selected +* Files written by older versions of Pentobi that use a deprecated format + for move properties are now automatically converted to the current format + on write + + +Version 2.1 (1 Jul 2012) +======================== + +* Bugfix: File was erroneously marked as modified if a multiline comment + was shown and the platform that was used to create the file had + Windows-style end of line convention and the platform on which the file + was shown had Unix-style. +* Fixed the corruption of non-ASCII characters in game files on some + platforms. +* Fixed a case where the program froze instead of showing an error on + certain syntax errors in the SGF file. +* Fixed duplicate menu shortcut in German translation +* Fixed too high floating point tolerance in unit tests. + + +Version 2.0 (22 May 2012) +========================= + +* No more popup messages if a color has no more moves; + instead, score points of this color are underlined + (feature request #3431031) +* Newly supported game variant Junior +* Improved playing strength. Number of levels increased to 7. + Level 7 is about the same speed as the old level 6 but stronger. +* New game analysis function that shows a graph with the estimated + value of each position in a game (menu item "Computer/Analyze Game") +* Support for setup properties in blksgf files (note that files + with setup properties cannot be read by older versions of + Pentobi). A new setup mode can be used to create files that start + with a setup position including positions that cannot occur in + real games (e.g. for puzzles or Blokus art) +* New menu items for editing the game tree: "Delete All Variations", + "Keep Only Position", "Keep Only Subtree", "Move Variation Up/Down", + "Truncate Children" +* Variations are now displayed by appending a letter to the move number + instead of underlining +* Added a toolbar button for fast selection of the computer colors + without having to use the window menu. +* User manual is no longer compiled into the resources of the + executable but installed in the installation data directory +* Open a console for stderr output on Windows if Pentobi is + invoked with option --verbose +* New option --memory to make Pentobi run on systems with low + memory at the cost of reduced playing strength. +* Use standard icons from theme + + +Version 1.2 (17 Apr 2012) +========================= + +* Bugfix: program sometimes hung or crashed when generating a + move in early game Trigon positions especially when there + were no legal moves with any of the large pieces +* Bugfix: file modified marker was not set on certain changes + (Make Main Variation, comment changed) +* Bugfix: game info dialog showed wrong player labels in Trigon + and Trigon Three-Player * Minor other bugfixes in the code +* Reverted the change that used the SVG icon for setting the + window icon because it created an unwanted dependency on the + Qt SVG plugin. +* Made Save menu item and tool button active if game is modified + even if no file name is associated with the current game +* Made the code compile without warnings with GCC -Wunused +* Made "make post-install" continue even if some commands fail. + + +Version 1.1 (10 Mar 2012) +========================= + +* File is now immediately visible in Recent Files menu after + saving under a new name. +* Fixed several cases where the program crashed instead of showing + an error message if the opened file was invalid. The error + message now also has a Show Details button to show the reason + why the file could not be loaded. +* Fixed a bug that distorted the position values reported with + --verbose if a subtree from a previous search was reused +* Fixed exception in tools/twogtp/analyze.py if option -r was used +* Minor fixes in computer player engine +* Added explaining label to computer color dialog because window + title is not visible in all L&F's +* Accept pass moves (empty value) in files. Although the current + Blokus SGF documentation does not specify if they should be + allowed, they might be used in the future and are used in files + written by early (unreleased) versions of Pentobi +* Extended the file format documentation by a hint how to put + blksgf files on web servers +* Smaller icons for piece manipulation buttons +* Fixed computation of the font bounding box in the score display +* Set option -std=c++0x in CMakeLists.txt if compiler is CLang +* Removed duplicate pentobi.png in directories data and src/pentobi; + The file pentobi.svg was moved from data to src/pentobi and is + now used for setting the window icon of Pentobi + + +Version 1.0 (1 Jan 2012) +======================== + +* Support for game variant Trigon Three-Player +* Change directory for autosave file to use AppData + (on Windows) or XDG_DATA_HOME (on other systems) +* Changed Back to Main Variation to go to the last move + in the main variation that had a variation, not to the + last position in the main variation +* Changed variation string in status bar to contain + information about the move numbers at the branching points +* Fixed small rendering errors +* New menu item Find Next Comment +* Added chapters about the main window and menu items to + the user manual +* Fix bug: computer color dialog did not set colors correctly + in game variant Trigon +* Show error message instead of crashing if the SGF file + contains invalid move properties +* Lowered the required version of the Boost libraries in + CMakeLists.txt from 1.45 to 1.40 such that Pentobi can + be compiled on Debian 6.0. + Note: some versions of Boost cause compilation errors if + used with certain versions of GCC and option -std=c++0x + (e.g. the combinations GCC 4.4/Boost 1.40 in Ubuntu 10.04 + and GCC 4.4/Boost 1.42 in Debian 6.0 work but the combination + GCC 4.5/Boost 1.42 in Ubuntu 11.04 causes errors). +* Changed installation directories according to Filesystem + Hierarchy Standard (/usr/bin to /usr/games, /usr/share to + /usr/share/games) +* New CMake option PENTOBI_REGISTER_GNOME2_THUMBNAILER for + disabling the installation of files for registering the Pentobi + thumbnailer on Gnome 2 +* Install man pages for pentobi and pentobi-thumbnailer on Unix + systems + + +Version 0.3 (2 Dec 2011) +======================== + +* Support for the game variants Trigon and Trigon Two-Player +* Fixed saving/opening files if file name contained non-ASCII characters and + the system used an encoding other than Latin1 +* The score numbers now show the total player and color scores instead of + on-board and bonus points separately (feature request #3431039) +* New menu item "Edit/Select Next Color" that allows to enter moves independent + of the color to play on the board (feature request #3441299) +* Slightly changed file format to use single-valued move properties as used in + other games supported by SGF. Files written by Pentobi 0.2 can still be read. + + +Version 0.2 (17 Oct 2011) +========================= + +* German translation +* Display sum score for both player colors in game variant Classic Two-Player +* Slightly changed file format to conform to the proposed version 5 of SGF that + requires digits for move properties in multi-player games. Files written by + Pentobi 0.1 can still be read. +* Support for move annotation symbols +* Store and edit additional game information (player names, date) +* New menu items Ten Moves Backward/Forward, Go to Move, Undo Move +* Underline move numbers if there are alternative variations +* Show move number, total number of moves and current variation in status bar +* Faster play in higher levels, especially of opening moves +* Make thumbnailer for Blokus files work under Gnome 3 +* Fix broken compilation with GCC 4.6.1 (bug #3420555) + + +Version 0.1 (15 Jul 2011) +========================= + +Initial release. diff --git a/README b/README new file mode 100644 index 0000000..acb622a --- /dev/null +++ b/README @@ -0,0 +1,9 @@ +Pentobi is a computer opponent for the board game Blokus. + +Copyright (C) 2011-2016 Markus Enzenberger + +See the file COPYING for license information. +See the file INSTALL for instructions about how to build and install the +program from the sources. +See the file NEWS for release notes. +The homepage of Pentobi is at http://pentobi.sourceforge.net/ diff --git a/config.h.in b/config.h.in new file mode 100644 index 0000000..774b93e --- /dev/null +++ b/config.h.in @@ -0,0 +1,30 @@ +/* Define to 1 if you have the header file. */ +#cmakedefine01 HAVE_UNISTD_H + +/* Define to 1 if you have the header file. */ +#cmakedefine01 HAVE_SYS_TIMES_H + +/* Define to 1 if you have the header file. */ +#cmakedefine01 HAVE_SYS_SYSCTL_H + +/* Version number of package */ +#define VERSION "@PENTOBI_VERSION@" + +/* Define if the MCTS search does not need to support multi-threading. + This makes the search slightly faster on single-threaded systems. */ +#cmakedefine LIBBOARDGAME_MCTS_SINGLE_THREAD + +/* Floating type for Monte-Carlo tree search values (float|double) */ +#define LIBPENTOBI_MCTS_FLOAT_TYPE @LIBPENTOBI_MCTS_FLOAT_TYPE@ + +/* Build for systems with low memory and slow CPU. */ +#cmakedefine01 PENTOBI_LOW_RESOURCES + +/* Directory containing opening books. */ +#cmakedefine PENTOBI_BOOKS_DIR "@PENTOBI_BOOKS_DIR@" + +/** Location of the Pentobi user manual. */ +#cmakedefine PENTOBI_HELP_DIR "@PENTOBI_HELP_DIR@" + +/** Location of the translations directory. */ +#cmakedefine PENTOBI_TRANSLATIONS "@PENTOBI_TRANSLATIONS@" diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt new file mode 100644 index 0000000..9fef947 --- /dev/null +++ b/data/CMakeLists.txt @@ -0,0 +1,51 @@ +if(PENTOBI_BUILD_GUI AND NOT WIN32) + +add_custom_target( + pentobi-64.png ALL + COMMAND convert ${CMAKE_SOURCE_DIR}/src/pentobi/icons/pentobi-64.svg pentobi-64.png + DEPENDS ${CMAKE_SOURCE_DIR}/src/pentobi/icons/pentobi-64.svg + ) +add_custom_target( + application-x-blokus-sgf.png ALL + COMMAND convert ${CMAKE_CURRENT_SOURCE_DIR}/application-x-blokus-sgf.svg application-x-blokus-sgf.png + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/application-x-blokus-sgf.svg + ) +add_custom_target( + application-x-blokus-sgf-16.png ALL + COMMAND convert ${CMAKE_CURRENT_SOURCE_DIR}/application-x-blokus-sgf-16.svg application-x-blokus-sgf-16.png + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/application-x-blokus-sgf-16.svg + ) + +configure_file(pentobi.desktop.in pentobi.desktop @ONLY) +configure_file(pentobi.thumbnailer.in pentobi.thumbnailer @ONLY) +configure_file(pentobi.appdata.xml.in pentobi.appdata.xml @ONLY) +install(FILES ${CMAKE_BINARY_DIR}/src/pentobi/icons/pentobi.png + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/48x48/apps) +install(FILES ${CMAKE_BINARY_DIR}/src/pentobi/icons/pentobi-16.png + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/16x16/apps + RENAME pentobi.png) +install(FILES ${CMAKE_BINARY_DIR}/src/pentobi/icons/pentobi-32.png + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/32x32/apps + RENAME pentobi.png) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/pentobi-64.png + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/64x64/apps + RENAME pentobi.png) +install(FILES ${CMAKE_SOURCE_DIR}/src/pentobi/icons/pentobi.svg + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/apps) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/application-x-blokus-sgf.png + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/48x48/mimetypes) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/application-x-blokus-sgf-16.png + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/16x16/mimetypes + RENAME application-x-blokus-sgf.png) +install(FILES application-x-blokus-sgf.svg + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/mimetypes) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/pentobi.desktop + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/pentobi.thumbnailer + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/thumbnailers) +install(FILES pentobi-mime.xml + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/mime/packages) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/pentobi.appdata.xml + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/appdata) + +endif(PENTOBI_BUILD_GUI AND NOT WIN32) diff --git a/data/application-x-blokus-sgf-16.svg b/data/application-x-blokus-sgf-16.svg new file mode 100644 index 0000000..d0d9743 --- /dev/null +++ b/data/application-x-blokus-sgf-16.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/data/application-x-blokus-sgf-32.svg b/data/application-x-blokus-sgf-32.svg new file mode 100644 index 0000000..935183a --- /dev/null +++ b/data/application-x-blokus-sgf-32.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/application-x-blokus-sgf-64.svg b/data/application-x-blokus-sgf-64.svg new file mode 100644 index 0000000..495a843 --- /dev/null +++ b/data/application-x-blokus-sgf-64.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/application-x-blokus-sgf.svg b/data/application-x-blokus-sgf.svg new file mode 100644 index 0000000..c903262 --- /dev/null +++ b/data/application-x-blokus-sgf.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/pentobi-mime.xml b/data/pentobi-mime.xml new file mode 100644 index 0000000..fe420d7 --- /dev/null +++ b/data/pentobi-mime.xml @@ -0,0 +1,23 @@ + + + +Blokus game +Blokus-Partie + + + + + + + + + + + + + + + + + + diff --git a/data/pentobi.appdata.xml.in b/data/pentobi.appdata.xml.in new file mode 100644 index 0000000..c3c691b --- /dev/null +++ b/data/pentobi.appdata.xml.in @@ -0,0 +1,91 @@ + + + pentobi.desktop + CC0-1.0 + GPL-3.0+ + Pentobi + Pentobi + Computer opponent for the board game Blokus + Computer-Gegner für das Brettspiel Blokus + + +

Pentobi is a computer opponent for the board game Blokus. It has a + strong Blokus engine with 9 different playing levels. The supported game + variants are: Classic, Duo, Trigon, Junior, Nexos, Callisto.

+

Pentobi ist ein Computer-Gegner für das Brettspiel Blokus. + Es hat eine spielstarke Blokus-Engine mit 9 verschiedenen Spielstufen. + Die unterstützten Spielvarianten sind : Klassisch, Duo, Trigon, Junior, + Nexos, Callisto.

+ +

Players can determine their strength by playing rated games against the + computer and use a game analysis function. Games can be saved in Smart Game + Format with comments and move variations.

+

Spieler können ihre Spielstärke ermitteln, indem sie + gewertete Spiele gegen den Computer spielen, und eine Spielanalysefunktion + benutzen. Spiele können im Smart-Game-Format gespeichert werden mit + Kommentaren und Zugvarianten.

+ +

System requirements: 1 GB RAM, 1 GHz CPU (4 GB RAM, 2 GHz dual-core or + faster CPU recommended for playing level 9).

+

Systemminima: 1 GB RAM, 1 GHz CPU (4 GB RAM, 2 GHz + Dual-Core- oder schnellere CPU empfohlen für Spielstufe 9).

+ +

Trademark disclaimer: The trademark Blokus and other trademarks referred + to are property of their respective trademark holders. The trademark + holders are not affiliated with the author of the program Pentobi.

+

Hinweis zu Markennamen: Der Markenname Blokus und andere + erwähnte Marken sind Eigentum ihrer jeweiligen Markeninhaber. Die + Markeninhaber stehen in keiner Verbindung mit dem Autor des Programms + Pentobi.

+
+ + + + + http://pentobi.sourceforge.net/pentobi-classic.png + Game variant Classic + Spielvariante Klassisch + + + + http://pentobi.sourceforge.net/pentobi-duo.png + Game variant Duo + Spielvariante Duo + + + + http://pentobi.sourceforge.net/pentobi-trigon.png + Game variant Trigon + Spielvariante Trigon + + + + http://pentobi.sourceforge.net/pentobi-nexos.png + Game variant Nexos + Spielvariante Nexos + + + + http://pentobi.sourceforge.net/pentobi-callisto.png + Game variant Callisto + Spielvariante Callisto + + + + http://pentobi.sourceforge.net/ + https://sourceforge.net/p/pentobi/bugs/ + https://sourceforge.net/p/pentobi/donate/ + Markus Enzenberger + enz@users.sourceforge.net + + + pentobi + + + application/x-blokus-sgf + + + + + +
diff --git a/data/pentobi.desktop.in b/data/pentobi.desktop.in new file mode 100755 index 0000000..0992025 --- /dev/null +++ b/data/pentobi.desktop.in @@ -0,0 +1,12 @@ +[Desktop Entry] +Name=Pentobi +GenericName=Computer Opponent for Blokus +GenericName[de]=Computer-Gegner für Blokus +Comment=Computer opponent for the board game Blokus +Comment[de]=Computer-Gegner für das Brettspiel Blokus +Keywords=Blokus;Blokus Duo;Blokus Trigon;Blokus Junior;Nexos;Callisto; +Exec=@CMAKE_INSTALL_FULL_BINDIR@/pentobi %f +Icon=pentobi +Type=Application +Categories=Game;BoardGame; +MimeType=application/x-blokus-sgf; diff --git a/data/pentobi.thumbnailer.in b/data/pentobi.thumbnailer.in new file mode 100644 index 0000000..dd95f90 --- /dev/null +++ b/data/pentobi.thumbnailer.in @@ -0,0 +1,3 @@ +[Thumbnailer Entry] +Exec=@CMAKE_INSTALL_FULL_BINDIR@/pentobi-thumbnailer --size %s %i %o +MimeType=application/x-blokus-sgf; diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt new file mode 100644 index 0000000..5cf3005 --- /dev/null +++ b/doc/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(man) diff --git a/doc/blksgf/Pentobi-SGF.html b/doc/blksgf/Pentobi-SGF.html new file mode 100644 index 0000000..76b40a9 --- /dev/null +++ b/doc/blksgf/Pentobi-SGF.html @@ -0,0 +1,147 @@ + + + +Pentobi SGF Files + + + +

Pentobi SGF Files

+
Author: Markus Enzenberger
+Last modified: 2016-11-27
+

This document describes the file format for Blokus game records as used by the +program Pentobi. The most recent +version of this document can be found in the source code distribution of +Pentobi in the folder pentobi/doc/blksgf.

+

Introduction

+

The file format is a derivative of the Smart Game Format (SGF). The current SGF +version 4 does not define standard properties for Blokus. Therefore, a number +of game-specific properties and value types had to be defined. The definitions +follow the recommendations of SGF 4 and the proposals for multi-player games +from the discussions +about the future SGF version 5.

+

File Extension and MIME Type

+

The file extension .blksgf and the MIME type +application/x-blokus-sgf are used for Blokus SGF files.

+

Note
+Since this is a non-standard MIME type, links to Blokus SGF files on web +servers will not automatically open the file with Pentobi even if Pentobi is +installed locally and registered as a handler for Blokus SGF files. To make +this work, you can put a file named .htaccess on the web server in the +same directory that contains the .blksgf files or in one of its parent +directories. This file needs to contain the line:

+
AddType application/x-blokus-sgf +blksgf
+

Character Set

+

Although not specific to Blokus, it is recommended to use UTF-8 as the character set. Pentobi +always writes files in UTF-8 and indicates that with the CA property. +Pentobi can read SGF files encoded in UTF-8 or ISO-8859-1 (Latin1). Other +character sets are currently not supported. As specified by the SGF standard, +ISO-8859-1 is assumed for files without CA property.

+

Game Property

+

Since there is no number for Blokus defined in SGF 4, a string instead of a +number is used as the value for the GM property. Currently, the +following strings are used: Blokus, Blokus Two-Player, Blokus Three-Player, +Blokus Duo, Blokus Trigon, Blokus Trigon Two-Player, Blokus Trigon +Three-Player, Blokus Junior, Nexos, Nexos Two-Player, Callisto, Callisto +Two-Player, Callisto Three-Player.

+The strings are case-sensitive, words are separated by exactly one space and no +additional whitespace at the beginning or end of the string is allowed. +

Color and Player Properties

+

In game variants with two players and two colors, B denotes the +first player or color, W the second player or color. In game variants +with three or four players and one color per player, 1, 2, +3, 4 denote the first, second, third, and fourth player or +color. In game variants with two players and four colors, B denotes +the first player, W the second player, and 1, 2, +3, 4 denote the first, second, third, and fourth color. This +applies to move properties and properties related to a player or a color.

+

Example 1: in the game variant Blokus Two-Player PW is the name of +the first player, and 1 is a move of the first color.

+

Example 2: in the game variant Blokus Two-Player, one could either use the +BL, WL properties to indicate the time left for a player, if +the game is played with a time limit for each player, or one could use the +1L, 2L, 3L, 4L properties to indicate the +time left for a color, if the game is played with a time limit for each color. +(This is only an example how the properties should be interpreted. Pentobi +currently has no support for game clocks.)

+

Note
+Pentobi versions before 0.2 used the properties BLUE, YELLOW, +RED, GREEN in the four-color game variants, which did not +reflect the current state of discussion for SGF 5. Pentobi 12.0 erroneously +used multi-player properties for two-player Callisto. Current versions of +Pentobi can still read games written by older versions and will convert old +properties.

+

Coordinate System

+

Fields on the board (called points in SGF) are identified by a +case-insensitive string with a letter for the column followed by a number for +the row. The letters start with 'a', the numbers start with '1'. The lower left +corner of the board is 'a1'. The strings are not allowed to contain +whitespaces. Note that, unlike the common convention in the game of Go, the +letter 'i' is used.

+

If there are more than 26 columns, the columns continue with 'aa', 'ab', +..., 'ba', 'bb', ... More than 26 columns are presently required for Trigon and +could also be required for future game variants on rectangular boards larger +than 26×26.

+

For Trigon, hexagonal boards are mapped to rectangular coordinates as in the +following example of a hexagon with edge size 3:

+
+       6     / \ / \ / \ / \
+       5   / \ / \ / \ / \ / \
+       4 / \ / \ / \ / \ / \ / \
+       3 \ / \ / \ / \ / \ / \ /
+       2   \ / \ / \ / \ / \ /
+       1     \ / \ / \ / \ /
+          a b c d e f g h i j k
+
+

In Nexos, the 13×13 line grid is mapped to a 25×25 coordinate system, in +which rows with horizontal line segments and intersections alternate with rows +with vertical line segments and holes:

+
+       6 |   |   |
+       5 + - + - + -
+       4 |   |   |
+       3 + - + - + -
+       2 |   |   |
+       1 + - + - + -
+         a b c d e f
+
+

Move Properties

+

The value of a move property is a string with the coordinates of the played +piece on the board separated by commas. No whitespace characters are allowed +before, after, or in-between the coordinates.

+

Pentobi currently does not require a certain order of the coordinates of a +move. However, move properties should be written with an ordered list of +coordinates (using the order a1, b1, …, a2, b2, …) such that each move has a +unique string representation.

+

Example: B[f9,e10,f10,g10,f11]

+

In Nexos, moves contain only the coordinates of line segments occupied by +the piece, no coordinates of junctions.

+

Note
+Old versions of Pentobi (before version 0.3) used to represent moves by a list +of points, which did not follow the convention used by other games in SGF to +use single-value properties for moves. Current versions of Pentobi can still +read games containing the old move property values but they are deprecated and +should no longer be used.

+

Setup Properties

+

The setup properties AB, AW, A1, A2, +A3, A4 can be used to place several pieces simultaneously on +the board. The setup property AE can be used to remove pieces from the +board. All these properties can have multiple values, each value represents a +piece by its coordinates as in the move properties. The PL can be used +to set the color to play in a setup position.

+

Example:
+AB[e8,e9,f9,d10,e10][g6,f7,g7,h7,g8]
+AW[i4,h5,i5,j5,i6][j7,j8,j9,k9,j10]
+PL[B]

+

Note
+Older versions of Pentobi (before version 2.0) did not support setup +properties, you need a newer version of Pentobi to read such files. Currently, +Pentobi is able to read files with setup properties in any node, but can create +only files with setup in the root node.

+ + diff --git a/doc/doxygen/.gitignore b/doc/doxygen/.gitignore new file mode 100644 index 0000000..1b0a5aa --- /dev/null +++ b/doc/doxygen/.gitignore @@ -0,0 +1,2 @@ +doxygen_sqlite3.db +html/ diff --git a/doc/doxygen/Doxyfile b/doc/doxygen/Doxyfile new file mode 100644 index 0000000..326986f --- /dev/null +++ b/doc/doxygen/Doxyfile @@ -0,0 +1,317 @@ +# Doxyfile 1.8.9.1 + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- +DOXYFILE_ENCODING = UTF-8 +PROJECT_NAME = Pentobi +PROJECT_NUMBER = +PROJECT_BRIEF = +PROJECT_LOGO = +OUTPUT_DIRECTORY = +CREATE_SUBDIRS = NO +ALLOW_UNICODE_NAMES = NO +OUTPUT_LANGUAGE = English +BRIEF_MEMBER_DESC = YES +REPEAT_BRIEF = YES +ABBREVIATE_BRIEF = +ALWAYS_DETAILED_SEC = NO +INLINE_INHERITED_MEMB = NO +FULL_PATH_NAMES = YES +STRIP_FROM_PATH = +STRIP_FROM_INC_PATH = +SHORT_NAMES = NO +JAVADOC_AUTOBRIEF = YES +QT_AUTOBRIEF = NO +MULTILINE_CPP_IS_BRIEF = NO +INHERIT_DOCS = YES +SEPARATE_MEMBER_PAGES = NO +TAB_SIZE = 8 +ALIASES = +TCL_SUBST = +OPTIMIZE_OUTPUT_FOR_C = NO +OPTIMIZE_OUTPUT_JAVA = NO +OPTIMIZE_FOR_FORTRAN = NO +OPTIMIZE_OUTPUT_VHDL = NO +EXTENSION_MAPPING = +MARKDOWN_SUPPORT = YES +AUTOLINK_SUPPORT = YES +BUILTIN_STL_SUPPORT = NO +CPP_CLI_SUPPORT = NO +SIP_SUPPORT = NO +IDL_PROPERTY_SUPPORT = YES +DISTRIBUTE_GROUP_DOC = NO +SUBGROUPING = YES +INLINE_GROUPED_CLASSES = NO +INLINE_SIMPLE_STRUCTS = NO +TYPEDEF_HIDES_STRUCT = NO +LOOKUP_CACHE_SIZE = 0 +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- +EXTRACT_ALL = YES +EXTRACT_PRIVATE = NO +EXTRACT_PACKAGE = NO +EXTRACT_STATIC = NO +EXTRACT_LOCAL_CLASSES = YES +EXTRACT_LOCAL_METHODS = NO +EXTRACT_ANON_NSPACES = NO +HIDE_UNDOC_MEMBERS = NO +HIDE_UNDOC_CLASSES = NO +HIDE_FRIEND_COMPOUNDS = NO +HIDE_IN_BODY_DOCS = NO +INTERNAL_DOCS = NO +CASE_SENSE_NAMES = YES +HIDE_SCOPE_NAMES = NO +HIDE_COMPOUND_REFERENCE= NO +SHOW_INCLUDE_FILES = YES +SHOW_GROUPED_MEMB_INC = NO +FORCE_LOCAL_INCLUDES = NO +INLINE_INFO = YES +SORT_MEMBER_DOCS = YES +SORT_BRIEF_DOCS = NO +SORT_MEMBERS_CTORS_1ST = NO +SORT_GROUP_NAMES = NO +SORT_BY_SCOPE_NAME = NO +STRICT_PROTO_MATCHING = NO +GENERATE_TODOLIST = YES +GENERATE_TESTLIST = YES +GENERATE_BUGLIST = YES +GENERATE_DEPRECATEDLIST= YES +ENABLED_SECTIONS = +MAX_INITIALIZER_LINES = 30 +SHOW_USED_FILES = YES +SHOW_FILES = YES +SHOW_NAMESPACES = YES +FILE_VERSION_FILTER = +LAYOUT_FILE = +CITE_BIB_FILES = +#--------------------------------------------------------------------------- +# Configuration options related to warning and progress messages +#--------------------------------------------------------------------------- +QUIET = NO +WARNINGS = YES +WARN_IF_UNDOCUMENTED = YES +WARN_IF_DOC_ERROR = YES +WARN_NO_PARAMDOC = NO +WARN_FORMAT = "$file:$line: $text" +WARN_LOGFILE = +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- +INPUT = ../../src +INPUT_ENCODING = UTF-8 +FILE_PATTERNS = *.h \ + *.cpp +RECURSIVE = YES +EXCLUDE = +EXCLUDE_SYMLINKS = NO +EXCLUDE_PATTERNS = +EXCLUDE_SYMBOLS = +EXAMPLE_PATH = +EXAMPLE_PATTERNS = +EXAMPLE_RECURSIVE = NO +IMAGE_PATH = +INPUT_FILTER = +FILTER_PATTERNS = +FILTER_SOURCE_FILES = NO +FILTER_SOURCE_PATTERNS = +USE_MDFILE_AS_MAINPAGE = +#--------------------------------------------------------------------------- +# Configuration options related to source browsing +#--------------------------------------------------------------------------- +SOURCE_BROWSER = NO +INLINE_SOURCES = NO +STRIP_CODE_COMMENTS = YES +REFERENCED_BY_RELATION = NO +REFERENCES_RELATION = NO +REFERENCES_LINK_SOURCE = YES +SOURCE_TOOLTIPS = YES +USE_HTAGS = NO +VERBATIM_HEADERS = YES +CLANG_ASSISTED_PARSING = NO +CLANG_OPTIONS = +#--------------------------------------------------------------------------- +# Configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- +ALPHABETICAL_INDEX = NO +COLS_IN_ALPHA_INDEX = 5 +IGNORE_PREFIX = +#--------------------------------------------------------------------------- +# Configuration options related to the HTML output +#--------------------------------------------------------------------------- +GENERATE_HTML = YES +HTML_OUTPUT = html +HTML_FILE_EXTENSION = .html +HTML_HEADER = +HTML_FOOTER = footer.html +HTML_STYLESHEET = +HTML_EXTRA_STYLESHEET = +HTML_EXTRA_FILES = +HTML_COLORSTYLE_HUE = 220 +HTML_COLORSTYLE_SAT = 100 +HTML_COLORSTYLE_GAMMA = 80 +HTML_TIMESTAMP = YES +HTML_DYNAMIC_SECTIONS = NO +HTML_INDEX_NUM_ENTRIES = 100 +GENERATE_DOCSET = NO +DOCSET_FEEDNAME = "Doxygen generated docs" +DOCSET_BUNDLE_ID = org.doxygen.Project +DOCSET_PUBLISHER_ID = org.doxygen.Publisher +DOCSET_PUBLISHER_NAME = Publisher +GENERATE_HTMLHELP = NO +CHM_FILE = +HHC_LOCATION = +GENERATE_CHI = NO +CHM_INDEX_ENCODING = +BINARY_TOC = NO +TOC_EXPAND = NO +GENERATE_QHP = NO +QCH_FILE = +QHP_NAMESPACE = org.doxygen.Project +QHP_VIRTUAL_FOLDER = doc +QHP_CUST_FILTER_NAME = +QHP_CUST_FILTER_ATTRS = +QHP_SECT_FILTER_ATTRS = +QHG_LOCATION = +GENERATE_ECLIPSEHELP = NO +ECLIPSE_DOC_ID = org.doxygen.Project +DISABLE_INDEX = NO +GENERATE_TREEVIEW = NO +ENUM_VALUES_PER_LINE = 4 +TREEVIEW_WIDTH = 250 +EXT_LINKS_IN_WINDOW = NO +FORMULA_FONTSIZE = 10 +FORMULA_TRANSPARENT = YES +USE_MATHJAX = NO +MATHJAX_FORMAT = HTML-CSS +MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest +MATHJAX_EXTENSIONS = +MATHJAX_CODEFILE = +SEARCHENGINE = NO +SERVER_BASED_SEARCH = NO +EXTERNAL_SEARCH = NO +SEARCHENGINE_URL = +SEARCHDATA_FILE = searchdata.xml +EXTERNAL_SEARCH_ID = +EXTRA_SEARCH_MAPPINGS = +#--------------------------------------------------------------------------- +# Configuration options related to the LaTeX output +#--------------------------------------------------------------------------- +GENERATE_LATEX = NO +LATEX_OUTPUT = latex +LATEX_CMD_NAME = latex +MAKEINDEX_CMD_NAME = makeindex +COMPACT_LATEX = NO +PAPER_TYPE = a4wide +EXTRA_PACKAGES = +LATEX_HEADER = +LATEX_FOOTER = +LATEX_EXTRA_STYLESHEET = +LATEX_EXTRA_FILES = +PDF_HYPERLINKS = YES +USE_PDFLATEX = YES +LATEX_BATCHMODE = NO +LATEX_HIDE_INDICES = NO +LATEX_SOURCE_CODE = NO +LATEX_BIB_STYLE = plain +#--------------------------------------------------------------------------- +# Configuration options related to the RTF output +#--------------------------------------------------------------------------- +GENERATE_RTF = NO +RTF_OUTPUT = rtf +COMPACT_RTF = NO +RTF_HYPERLINKS = NO +RTF_STYLESHEET_FILE = +RTF_EXTENSIONS_FILE = +RTF_SOURCE_CODE = NO +#--------------------------------------------------------------------------- +# Configuration options related to the man page output +#--------------------------------------------------------------------------- +GENERATE_MAN = NO +MAN_OUTPUT = man +MAN_EXTENSION = .3 +MAN_SUBDIR = +MAN_LINKS = NO +#--------------------------------------------------------------------------- +# Configuration options related to the XML output +#--------------------------------------------------------------------------- +GENERATE_XML = NO +XML_OUTPUT = xml +XML_PROGRAMLISTING = YES +#--------------------------------------------------------------------------- +# Configuration options related to the DOCBOOK output +#--------------------------------------------------------------------------- +GENERATE_DOCBOOK = NO +DOCBOOK_OUTPUT = docbook +DOCBOOK_PROGRAMLISTING = NO +#--------------------------------------------------------------------------- +# Configuration options for the AutoGen Definitions output +#--------------------------------------------------------------------------- +GENERATE_AUTOGEN_DEF = NO +#--------------------------------------------------------------------------- +# Configuration options related to the Perl module output +#--------------------------------------------------------------------------- +GENERATE_PERLMOD = NO +PERLMOD_LATEX = NO +PERLMOD_PRETTY = YES +PERLMOD_MAKEVAR_PREFIX = +#--------------------------------------------------------------------------- +# Configuration options related to the preprocessor +#--------------------------------------------------------------------------- +ENABLE_PREPROCESSING = YES +MACRO_EXPANSION = NO +EXPAND_ONLY_PREDEF = NO +SEARCH_INCLUDES = YES +INCLUDE_PATH = +INCLUDE_FILE_PATTERNS = +PREDEFINED = HAVE_BOOST_THREAD +EXPAND_AS_DEFINED = +SKIP_FUNCTION_MACROS = YES +#--------------------------------------------------------------------------- +# Configuration options related to external references +#--------------------------------------------------------------------------- +TAGFILES = +GENERATE_TAGFILE = +ALLEXTERNALS = NO +EXTERNAL_GROUPS = YES +EXTERNAL_PAGES = YES +PERL_PATH = /usr/bin/perl +#--------------------------------------------------------------------------- +# Configuration options related to the dot tool +#--------------------------------------------------------------------------- +CLASS_DIAGRAMS = YES +MSCGEN_PATH = +DIA_PATH = +HIDE_UNDOC_RELATIONS = YES +HAVE_DOT = NO +DOT_NUM_THREADS = 0 +DOT_FONTNAME = Helvetica +DOT_FONTSIZE = 10 +DOT_FONTPATH = +CLASS_GRAPH = YES +COLLABORATION_GRAPH = YES +GROUP_GRAPHS = YES +UML_LOOK = NO +UML_LIMIT_NUM_FIELDS = 10 +TEMPLATE_RELATIONS = NO +INCLUDE_GRAPH = YES +INCLUDED_BY_GRAPH = YES +CALL_GRAPH = NO +CALLER_GRAPH = NO +GRAPHICAL_HIERARCHY = YES +DIRECTORY_GRAPH = YES +DOT_IMAGE_FORMAT = png +INTERACTIVE_SVG = NO +DOT_PATH = +DOTFILE_DIRS = +MSCFILE_DIRS = +DIAFILE_DIRS = +PLANTUML_JAR_PATH = +PLANTUML_INCLUDE_PATH = +DOT_GRAPH_MAX_NODES = 50 +MAX_DOT_GRAPH_DEPTH = 0 +DOT_TRANSPARENT = YES +DOT_MULTI_TARGETS = NO +GENERATE_LEGEND = YES +DOT_CLEANUP = YES diff --git a/doc/doxygen/footer.html b/doc/doxygen/footer.html new file mode 100644 index 0000000..13af170 --- /dev/null +++ b/doc/doxygen/footer.html @@ -0,0 +1,8 @@ +

+


+
+$date Doxygen $doxygenversion +
+

+ + diff --git a/doc/gtp/Pentobi-GTP.html b/doc/gtp/Pentobi-GTP.html new file mode 100644 index 0000000..6864272 --- /dev/null +++ b/doc/gtp/Pentobi-GTP.html @@ -0,0 +1,321 @@ + + + +Pentobi GTP Interface + + + +

Pentobi GTP Interface

+
Author: Markus Enzenberger
+

This document describes the text-based interface to the engine of the Blokus +program Pentobi. The interface is +an adaption of the Go Text +Protocol (GTP) and allows controller programs to use the engine in an +automated way without the GUI. The most recent version of this document can be +found in the source code distribution of Pentobi in the folder +pentobi/doc/gtp.

+

Go Text Protocol

+

The Go Text Protocol is a simple text-based protocol. The engine reads +single-line commands from its standard input stream and writes multi-line +responses to its standard output stream. The first character of a response is a +status character: = for success, ? for failure, followed by +the actual response. The response ends with two consecutive newline characters. +See the GTP +specification for details.

+

Controllers

+

To use the engine from a controller program, the controller typically +creates a child process by running pentobi-gtp and then sends commands +and receives responses through the input/output streams of the child process. +How this is done, depends on the platform (programming language and/or +operating system). In Java, for example, a child process can be created with +java.lang.Runtime.exec().

+

Note that the input/output streams of child processes are often fully +buffered. You should explicitly flush the output stream after sending a +command. Another caveat is that pentobi-gtp writes debugging +information to its standard error stream. On some platforms the standard error +stream of the child process is automatically connected to the standard error +stream of its parent process. If not (this happens for example in Java), the +controller needs to read everything from the standard error stream of the child +process. This can be done for example by running a separate thread in the +parent process that has a simple read loop, which writes everything that it +reads to its own standard error stream or discards it. Otherwise the child +process will block as soon as the buffer for its standard error stream is full. +Alternatively, you can disable debugging output of pentobi-gtp with +the command line option --quiet, but it is generally better to assume +that a GTP engine writes text to standard error.

+

An example for a controller written in C++ for Linux is included in Pentobi +since version 9.0 in src/twogtp. The controller starts two GTP engines +and plays a number of Blokus games between them. Older versions of Pentobi +included a Python script with a similar functionality in +tools/twogtp/twogtp.py.

+

Building

+

Since the GTP engine is a developer tool, building it is not enabled by +default. To enable it, run cmake with the option +-DPENTOBI_BUILD_GTP=ON. After building, there will be an executable in +the build directory named src/pentobi_gtp/pentobi-gtp. The GTP engine +requires only standard C++ and has no dependency on other libraries like Qt, +which is needed for the GUI version of Pentobi. If you only want to build the +GTP engine, you can disable building the GUI with +-DPENTOBI_BUILD_GUI=OFF.

+

Options

+

The following command-line options are supported by +pentobi-gtp:

+
+
--book file
+
Specify a file name for the opening book. Opening books are blksgf files +containing trees, in which moves that Pentobi should select are marked as good +moves with the corresponding SGF property (see the files in +src/books). If no opening book is specified and opening books are not +disabled, pentobi-gtp will automatically search for an opening book +for the current game variant in the directory of the executable using the same +file name conventions as in src/books. If no such file is found it +will print an error message to standard error and disable the use of opening +books.
+
--config,-c file
+
Load a file with GTP commands and execute them before starting the main +loop, which reads commands from standard input. This can be used for +configuration files that contain GTP commands for setting parameters of the +engine (see below).
+
--color
+
Use ANSI escape sequences to colorize the text output of boards (for +example in the response to the showboard command or with the +--showboard command line option).
+
--cputime
+
Use CPU time instead of wall time for time measurement. Currently, there is +no way to make Pentobi play with time limits, the levels are defined by the +number of simulations in the MCTS search, so this affects only the debugging +output, which prints the time used after each search.
+
--game,-g variant
+
Specify the game variant used at start-up. Valid arguments are classic, +classic_2, duo, trigon, trigon_2, trigon_3, junior or the abbreviations c, c2, +d, t, t2, t3, j. By default, the initial game variant is classic. The game +variant can also be changed at run-time with a GTP command. If only a single +game variant is used, it is slightly faster and saves memory if the engine is +started in the right variant compared to having it start with classic and then +changing it.
+
--help,-h
+
Print a list of the command-line options and exit.
+
--level,-l n
+
Set the level of playing strength to n. Valid values are 1 to 9.
+
--seed,-r n
+
Use n as the seed for the random generator. Specifying a random seed +will make the move generation deterministic as long as the search is +single-threaded.
+
--showboard
+
Automatically write a text representation of the current position to +standard error after each command that alters the position.
+
--nobook
+
Disable the use of opening books.
+
--noresign
+
Disable resignation. If resignation is desabled, the genmove +command will never respond with resign. Resignation can speed up the +playing of test games if only the win/loss information is wanted.
+
--quiet,-q
+
Do not print any debugging messages, errors or warnings to standard +error.
+
--threads n
+
Use n threads during the search. Note that the default is 1, unlike +in the GUI version of Pentobi, which sets the default according to the number +of hardware threads (CPUs, cores or virtual cores) available on the current +system. The reason is that, for example, using 2 threads makes the search twice +as fast but may lose a bit of playing strength compared to the single-threaded +search. Therefore, if the GTP engine is used to play many test games with +twogtp.py (which supports playing games in parallel), it is better to play the +games with single-threaded search in parallel than with multi-threaded search +sequentially. Using a large number of threads (e.g. more than 4 on game +variants with a small board or more than 8 on large boards) is untested and +might reduce the playing strength significantly compared to the single-threaded +search.
+
--version,-v
+
Print the version of Pentobi and exit.
+
+

Commands

+

Standard Commands

+

The following GTP commands have the same or an equivalent meaning as +specified by the GTP standard. Colors or players in arguments or responses are +represented as in the property IDs of blksgf files (B, W if +two players or colors; 1, 2, 3, 4 if more +than two). Moves in arguments or responses are represented as in the move +property values of blksgf files. See the specification for Pentobi SGF files for +details.

+
+
all_legal color
+
List all legal moves for a color.
+
clear_board
+
Clear the board and start a new game in the current game variant.
+
final_score
+
Get the score of a final board position. In two-player game variants, the +format of the response is as in the result property in the SGF standard for the +game of Go (e.g. B+2 if the first player wins with two points, or +0 for a draw). In game variants with more than two players, the +response is a list of the points for each player (e.g. +64 69 70 40). If the current position is not a final +position, the response is undefined.
+
genmove color
+
Generate and play a move for a given color in the current position. If the +color has no more moves, the response is pass. If resignation is not +disabled, the response is resign if the players is very likely to +lose. Otherwise the response is the move.
+
known_command command
+
The response is true if command is a GTP command supported +by the engine, false otherwise.
+
list_commands
+
List all supported GTP commands, one command per line.
+
loadsgf file [move_number]
+
Load a board position from a blksgf file with name file. If +move_number is specified, the board position will be set to the position +in the main variation of the file before the move with the given number +was played, otherwise to the last position in the main variation.
+
name
+
Return the name of the GTP engine (Pentobi).
+
play color move
+
Play a move for a given color in the current board position.
+
protocol_version
+
Return the version of the GTP protocol used (currently 2).
+
quit
+
Exit the command loop and quit the engine.
+
reg_genmove color
+
Like the genmove command, but only generates a move and does not +play it on the board.
+
showboard
+
Return a text representation of the current board position.
+
undo
+
Undo the last move played.
+
version
+
Return the version of Pentobi.
+
+

Generally Useful Extension Commands

+
+
cputime
+
Return the CPU time used by the engine since the start of the program.
+
cputime_diff
+
Return the CPU time used by the engine since the last call of cputime_diff +or start of the program if cputime_diff has not been called yet.
+
g
+
Shortcut for the genmove command with the color argument set to +the current color to play.
+
get_place color
+
Get the place of a given color in the list of scores in a final position +(e.g. in game variant Classic, 1 is the place with the highest score, 4 the one +with the lowest, if all players have a different score). If some colors have +the same score, they share the same place and the string shared is +appended to the place number.
+
get_value
+
Get an estimated value of the board position from the view point of the +color of the last generated move. The return value is a win/loss estimation +between 0 (loss) and 1 (win) as produced by the last search performed by the +engine. This command should only be used immediately after a +reg_genmove or genmove command, otherwise the result is +undefined. The value is not very meaningful at the lowest playing levels. Note +that no searches are performed if the opening book is used for a move +generation and there is currently no way to check if this was so. Therefore, +the opening book should be disabled if the get_value command is +used.
+
p move
+
Shortcut for the play command with the color argument set to the +current color to play.
+
param [key value]
+
Set or query parameters specific to the Pentobi engine that can be changed +at run-time. If no arguments are given, the response is a list of the current +value with one key/value pair per line, otherwise the parameter with the given +key will be set to the given value. Generally useful parameters are: +
+
+
avoid_symmetric_draw 0|1
+
In some game variants (Duo, Trigon_2), the second player can enforce a tie +by answering each move by its symmetric counterpart if the first players misses +the opportunity to break the symmetry in the center. Technically, exploiting +this mistake by the first player is a good strategy for the second player +because a draw is a good result considering the first-play advantage. However, +playing symmetrically could be considered bad style, so this behavior is +avoided (value 1) by default.
+
fixed_simulations n
+
Use exactly n MCTS simulations during a search. By default, the +search engine uses levels, which determine how many MCTS simulations are run +during a search, but as a function that increases with the move number (because +the simulations become much faster at the end of the game). For some +experiments, it can be desirable to use a fixed number of simulations for each +move. If this number is specified, the playing level is ignored.
+
use_book 0|1
+
Enable or disable the opening book.
+
+
+The other parameters are only interesting for developers.
+
param_base [key value]
+
Set or query basic parameters that are not specific to the Pentobi engine. +If no arguments are given, the response is a list of the current value with one +key/value pair per line, otherwise the parameter with the given key will be set +to the given value. +
+
+
accept_illegal 0|1
+
Accept move arguments to the play command that violate the rules +of the game. If disabled, the play command will respond with an error, +otherwise it will perform the moves.
+
resign 0|1
+
Allow the engine to respond with resign to the genmove +command.
+
+
+
+
set_game variant
+
Set the current game variant and clear the board. The argument is the name +of the game variant as in the game property value of blksgf files (e.g. +Blokus Duo, see the specification for Pentobi SGF files for +details).
+
set_random_seed n
+
Set the seed of the random generator to n. See the documentation for +the command-line option --seed.
+
+

Extension Commands for Developers

+The remaining commands are only interesting for developers. See Pentobi's +source code for details. +

Example

+

The following GTP session queries the engine name and version, plays and +generates a move in game variant Duo and shows the resulting board position. +Commands are printed in bold, responses in normal text.

+
+$ ./pentobi-gtp --quiet
+name
+= Pentobi
+
+version
+= 7.1
+
+set_game Blokus Duo
+=
+
+play b e8,d9,e9,f9,e10
+=
+
+genmove w
+= i4,h5,i5,j5,i6
+
+showboard
+=
+   A B C D E F G H I J K L M N
+14 . . . . . . . . . . . . . . 14  *Blue(X): 5
+13 . . . . . . . . . . . . . . 13  1 F L5 N P T5 U V5 W Y
+12 . . . . . . . . . . . . . . 12  Z5 I5 O T4 Z4 L4 I4 V3 I3 2
+11 . . . . . . . . . . . . . . 11
+10 . . . . X . . . . . . . . . 10  Green(O): 5
+ 9 . . . X X X . . . . . . . . 9   1 F L5 N P T5 U V5 W Y
+ 8 . . . . X . . . . . . . . . 8   Z5 I5 O T4 Z4 L4 I4 V3 I3 2
+ 7 . . . . . . . . . . . . . . 7
+ 6 . . . . . . . .>O . . . . . 6
+ 5 . . . . . . . O O O . . . . 5
+ 4 . . . . . . . . O . . . . . 4
+ 3 . . . . . . . . . . . . . . 3
+ 2 . . . . . . . . . . . . . . 2
+ 1 . . . . . . . . . . . . . . 1
+   A B C D E F G H I J K L M N
+
+quit
+=
+
+
+ + diff --git a/doc/man/CMakeLists.txt b/doc/man/CMakeLists.txt new file mode 100644 index 0000000..d85259e --- /dev/null +++ b/doc/man/CMakeLists.txt @@ -0,0 +1,6 @@ +configure_file(pentobi.6.in pentobi.6 @ONLY) +configure_file(pentobi-thumbnailer.6.in pentobi-thumbnailer.6 @ONLY) +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/pentobi.6 + ${CMAKE_CURRENT_BINARY_DIR}/pentobi-thumbnailer.6 + DESTINATION ${CMAKE_INSTALL_MANDIR}/man6) diff --git a/doc/man/pentobi-thumbnailer.6.in b/doc/man/pentobi-thumbnailer.6.in new file mode 100644 index 0000000..a82c26d --- /dev/null +++ b/doc/man/pentobi-thumbnailer.6.in @@ -0,0 +1,35 @@ +.TH PENTOBI-THUMBNAILER 6 "2013-06-21" "Pentobi @PENTOBI_VERSION@" "Pentobi command reference" + +.SH NAME +pentobi-thumbnailer \- thumbnailer for game records for the board game Blokus as used by the program Pentobi + +.SH SYNOPSIS +.B pentobi-thumbnailer +.RI [ options ] " input-file output-file" +.br + +.SH DESCRIPTION + +.B pentobi-thumbnailer +is part of the program Pentobi and intended to be used as a thumbnailer for +the Gnome desktop environment to generate previews of game files written by +Pentobi. + +The input file is a game file in Pentobi's SGF format as documented in +doc/blksgf/Pentobi-SGF.html in the Pentobi source package. +The output file is a thumbnail in PNG format. + +.SH OPTIONS +.TP +.B \-s, \-\-size +The size of the thumbnail. The default is 128. + +.SH EXIT STATUS +.TP +0 if the thumbnail generation succeeds, 1 on error. + +.SH SEE ALSO +.BR pentobi (6) + +.SH AUTHOR +Markus Enzenberger diff --git a/doc/man/pentobi.6.in b/doc/man/pentobi.6.in new file mode 100644 index 0000000..a7ce12b --- /dev/null +++ b/doc/man/pentobi.6.in @@ -0,0 +1,49 @@ +.TH PENTOBI 6 "2015-01-04" "Pentobi @PENTOBI_VERSION@" "Pentobi command reference" + +.SH NAME +pentobi \- computer opponent for the board game Blokus + +.SH SYNOPSIS +.B pentobi +.RI [ options ] " [file]" +.br + +.SH DESCRIPTION + +.B pentobi +is the command to invoke the program Pentobi, which is a graphical user +interface and computer opponent to play the board game Blokus. + +The command can take the name of a game file to open at startup as an optional +argument. +The game file is expected to be in Pentobi's SGF format as documented in +doc/blksgf/Pentobi-SGF.html in the Pentobi source package. + +.SH OPTIONS +.TP +.B \-\-maxlevel +Set the maximum playing level. Reducing this value reduces the amount +of memory used by the search, which can be useful to run Pentobi on systems +that have low memory or are too slow to use the highest levels. +By default, Pentobi currently allocates up to 2 GB (but not more than a third +of the physical memory available on the system). +Reducing the maximum level to 8 currently reduces this amount by a factor +of 6 and lower maximum levels even more. +.TP +.B \-\-threads +The number of threads to use in the search. By default, up to 4 threads are +used in the search depending on the number of hardware threads supported +by the current system. +Using more threads will speed up the move generation but using a very high +number of threads (e.g. more than 8) can degrade the playing strength +in higher playing levels. +.TP +.B \-\-verbose +Print internal information about the move generation and other debugging +information to standard error. + +.SH SEE ALSO +.BR pentobi-thumbnailer (6) + +.SH AUTHOR +Markus Enzenberger diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..4f2ccf2 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,40 @@ +add_subdirectory(books) +add_subdirectory(libboardgame_sys) +add_subdirectory(libboardgame_util) +add_subdirectory(libboardgame_sgf) +add_subdirectory(libboardgame_base) +add_subdirectory(libboardgame_mcts) +add_subdirectory(libpentobi_base) +add_subdirectory(libpentobi_mcts) + +if (PENTOBI_BUILD_GTP) + add_subdirectory(libboardgame_gtp) + add_subdirectory(pentobi_gtp) + if(HAVE_UNISTD_H AND NOT(WIN32)) + add_subdirectory(twogtp) + else() + message(STATUS "Not building twogtp, needs POSIX") + endif() +endif() + +if (PENTOBI_BUILD_TESTS) + add_subdirectory(libboardgame_test) + add_subdirectory(libboardgame_test_main) + add_subdirectory(unittest) +endif() + +if (PENTOBI_BUILD_GUI) + add_subdirectory(convert) + add_subdirectory(libpentobi_gui) + add_subdirectory(libpentobi_thumbnail) + add_subdirectory(pentobi_thumbnailer) + add_subdirectory(pentobi) + if(PENTOBI_BUILD_KDE_THUMBNAILER) + add_subdirectory(libpentobi_kde_thumbnailer) + add_subdirectory(pentobi_kde_thumbnailer) + endif() +endif() + +if (PENTOBI_BUILD_QML) + add_subdirectory(pentobi_qml) +endif() diff --git a/src/books/CMakeLists.txt b/src/books/CMakeLists.txt new file mode 100644 index 0000000..d440a5a --- /dev/null +++ b/src/books/CMakeLists.txt @@ -0,0 +1,17 @@ +# Install the opening book files. If you change the destination, you need to +# update the default for PENTOBI_BOOKS_DIR in the main CMakeLists.txt +install(FILES + book_callisto.blksgf + book_callisto_2.blksgf + book_callisto_3.blksgf + book_classic.blksgf + book_classic_2.blksgf + book_classic_3.blksgf + book_duo.blksgf + book_junior.blksgf + book_nexos.blksgf + book_nexos_2.blksgf + book_trigon.blksgf + book_trigon_2.blksgf + book_trigon_3.blksgf + DESTINATION ${CMAKE_INSTALL_DATADIR}/pentobi/books) diff --git a/src/books/book_callisto.blksgf b/src/books/book_callisto.blksgf new file mode 100644 index 0000000..c9bccbf --- /dev/null +++ b/src/books/book_callisto.blksgf @@ -0,0 +1,16 @@ +( +;GM[Callisto] +( + ;1[g11]TE[1] + ( + ;2[n10]TE[1] + ) + ( + ;2[n11]TE[1] + ) +) +( + ;1[h12]TE[1] + ;2[m9]TE[1] +) +) diff --git a/src/books/book_callisto_2.blksgf b/src/books/book_callisto_2.blksgf new file mode 100644 index 0000000..14f40fa --- /dev/null +++ b/src/books/book_callisto_2.blksgf @@ -0,0 +1,24 @@ +( +;GM[Callisto Two-Player] +( + ;B[e9]TE[1] + ( + ;W[l8]TE[1] + ) + ( + ;W[k7]TE[1] + ) +) +( + ;B[f10]TE[1] + ( + ;W[k7]TE[1] + ) + ( + ;W[j6]TE[1] + ) + ( + ;W[l8] + ) +) +) diff --git a/src/books/book_callisto_3.blksgf b/src/books/book_callisto_3.blksgf new file mode 100644 index 0000000..6109c33 --- /dev/null +++ b/src/books/book_callisto_3.blksgf @@ -0,0 +1,21 @@ +( +;GM[Callisto Three-Player] +( + ;1[g11]TE[1] + ( + ;2[n10]TE[1] + ) + ( + ;2[n11]TE[1] + ) +) +( + ;1[h12]TE[1] + ( + ;2[m9]TE[1] + ) + ( + ;2[l8]TE[1] + ) +) +) diff --git a/src/books/book_classic.blksgf b/src/books/book_classic.blksgf new file mode 100644 index 0000000..bf9ad9e --- /dev/null +++ b/src/books/book_classic.blksgf @@ -0,0 +1,14 @@ +( +;GM[Blokus] +( + ;1[a17,b17,a18,a19,a20]TE[1] + ;2[s17,t17,t18,t19,t20]TE[1] + ;3[t1,t2,t3,s4,t4]TE[1] +) +( + ;1[b17,a18,b18,a19,a20]TE[1] +) +( + ;1[a20,b20,c20,c19,c18]TE[1] +) +) diff --git a/src/books/book_classic_2.blksgf b/src/books/book_classic_2.blksgf new file mode 100644 index 0000000..debcbc7 --- /dev/null +++ b/src/books/book_classic_2.blksgf @@ -0,0 +1,891 @@ +( +;GM[Blokus Two-Player] +( + ;1[a17,b17,a18,a19,a20]TE[1] + ( + ;2[s17,t17,t18,t19,t20]TE[1] + ( + ;3[t1,t2,s3,t3,s4]TE[1] + ( + ;4[a1,b1,c1,d1,d2]TE[1] + ( + ;1[d14,e14,d15,c16,d16]TE[1] + ( + ;2[p14,q14,q15,q16,r16]TE[1] + ( + ;3[r5,p6,q6,r6,p7]TE[1] + ;4[e3,e4,f4,g4,g5]TE[1] + ;1[h11,g12,h12,f13,g13]TE[1] + ;2[o11,p11,n12,o12,o13]TE[2] + ;3[o8,m9,n9,o9,p9]TE[1] + ;4[h6,h7,i7,i8,j8]TE[1] + ;1[j9,k9,l9,i10,j10]TE[2] + ;2[k10,l10,m10,n10,m11]TE[1] + ;3[q10,q11,q12,p13,q13]TE[1] + ) + ( + ;3[r5,q6,r6,p7,q7]TE[1] + ;4[e3,f3,f4,g4,g5]TE[1] + ;1[g11,f12,g12,h12,f13]TE[1] + ;2[o11,p11,n12,o12,o13]TE[1] + ;3[n8,o8,n9,m10,n10]TE[1] + ;4[h6,h7,i7,j7,i8]TE[1] + ;1[k10,l10,i11,j11,k11]TE[1] + ;2[l12,k13,l13,m13,l14]TE[1] + ;3[p9,q9,q10,r10,q11]TE[1] + ;4[l7,k8,l8,m8,l9]TE[1] + ) + ) + ( + ;2[p14,p15,q15,q16,r16]TE[1] + ;3[r5,p6,q6,r6,p7]TE[1] + ( + ;4[e3,e4,f4,g4,g5]TE[1] + ;1[h11,g12,h12,f13,g13]TE[1] + ;2[o11,p11,n12,o12,o13]TE[1] + ;3[o8,m9,n9,o9,p9]TE[1] + ;4[h6,h7,i7,i8,j8] + ;1[j9,k9,l9,i10,j10]TE[2] + ;2[k10,l10,m10,n10,m11]TE[1] + ;3[q10,q11,q12,p13,q13]TE[2] + ) + ( + ;4[e3,f3,f4,g4,g5]TE[1] + ) + ) + ) + ( + ;1[e14,d15,e15,c16,d16]TE[1] + ) + ) + ( + ;4[a1,a2,b2,c2,c3]TE[1] + ;1[d14,e14,d15,c16,d16]TE[1] + ;2[p14,q14,q15,q16,r16]TE[1] + ( + ;3[r5,p6,q6,r6,p7]TE[1] + ;4[d4,d5,e5,e6,f6]TE[1] + ( + ;1[h11,g12,h12,f13,g13]TE[1] + ;2[o11,p11,n12,o12,o13]TE[2] + ;3[o8,m9,n9,o9,p9]TE[1] + ;4[h6,g7,h7,h8,i8]TE[1] + ( + ;1[j9,k9,l9,i10,j10]TE[2] + ) + ( + ;1[k9,l9,i10,j10,k10]TE[2] + ) + ) + ( + ;1[g11,h11,f12,g12,f13]TE[1] + ;2[o11,p11,n12,o12,o13]TE[1] + ;3[o8,o9,n10,o10,p10]TE[1] + ;4[g7,h7,h8,i8,j8]TE[1] + ;1[i10,j10,k10,l10,m10]TE[1] + ) + ) + ( + ;3[r5,q6,r6,p7,q7]TE[1] + ;4[d4,d5,e5,e6,f6]TE[1] + ;1[g11,f12,g12,h12,f13]TE[1] + ;2[m11,n11,n12,o12,o13]TE[1] + ;3[o8,o9,m10,n10,o10]TE[1] + ;4[g7,h7,h8,i8,j8]TE[1] + ;1[k10,l10,i11,j11,k11]TE[1] + ;2[i12,j12,k12,l12,j13]TE[1] + ;3[p11,p12,q12,r12,p13]TE[1] + ;4[k7,l7,m7,n7,o7]TE[1] + ) + ) + ( + ;4[a1,b1,b2,b3,c3]TE[1] + ;1[d14,e14,d15,c16,d16]TE[1] + ;2[p14,q14,q15,q16,r16]TE[1] + ;3[r5,p6,q6,r6,p7]TE[1] + ) + ( + ;4[a1,a2,a3,b3,c3]TE[1] + ;1[d14,e14,d15,c16,d16]TE[1] + ;2[p14,q14,q15,q16,r16]TE[1] + ;3[r5,p6,q6,r6,p7]TE[1] + ;4[d4,e4,e5,e6,f6]TE[1] + ;1[h11,g12,h12,f13,g13]TE[1] + ;2[o11,p11,n12,o12,o13]TE[2] + ;3[o8,m9,n9,o9,p9]TE[1] + ;4[g7,h7,h8,i8,j8]TE[1] + ( + ;1[j9,k9,l9,i10,j10]TE[2] + ;2[k10,l10,m10,n10,m11]TE[1] + ;3[q10,q11,q12,p13,q13]TE[1] + ) + ( + ;1[k9,l9,i10,j10,k10]TE[2] + ;2[l10,m10,n10,m11]TE[1] + ;3[q10,q11,q12,p13,q13]TE[1] + ) + ) + ) + ( + ;3[t1,t2,t3,s4,t4]TE[1] + ( + ;4[a1,b1,c1,d1,d2]TE[1] + ;1[d14,e14,d15,c16,d16]TE[1] + ( + ;2[p14,q14,q15,q16,r16]TE[1] + ;3[q5,r5,q6,p7,q7]TE[1] + ;4[e3,e4,f4,g4,g5]TE[1] + ;1[g11,h11,f12,g12,f13]TE[1] + ;2[m11,n11,n12,o12,o13]TE[1] + ;3[o8,o9,p9,n10,o10]TE[1] + ;4[h6,h7,i7,i8,j8]TE[1] + ;1[i10,j10,k10,l10,m10]TE[1] + ;2[r10,p11,q11,r11,q12]TE[1] + ;3[k7,k8,l8,l9,m9]TE[1] + ;4[l5,j6,k6,l6,m6]TE[1] + ) + ( + ;2[q14,p15,q15,q16,r16]TE[1] + ;3[r5,q6,r6,p7,q7]TE[1] + ;4[e3,f3,f4,g4,g5]TE[1] + ;1[g11,h11,f12,g12,f13]TE[1] + ;2[o12,n13,o13,p13,o14]TE[1] + ;3[o8,o9,n10,o10,p10]TE[1] + ;4[h6,h7,i7,j7,i8]TE[1] + ;1[i10,j10,k10,l10,m10]TE[1] + ;2[m11,n11,l12,m12]TE[1] + ;3[k7,k8,l8,m8,m9]TE[1] + ;4[h9,f10,g10,h10,f11]TE[1] + ) + ) + ( + ;4[a1,a2,b2,c2,c3]TE[1] + ;1[e14,c15,d15,e15,c16]TE[1] + ;2[p14,q14,q15,q16,r16]TE[1] + ;3[r5,q6,r6,p7,q7]TE[1] + ;4[d4,d5,e5,e6,f6]TE[1] + ;1[g11,h11,f12,g12,f13]TE[1] + ( + ;2[o11,p11,n12,o12,o13]TE[1] + ;3[o8,o9,n10,o10,p10]TE[1] + ;4[g7,h7,h8,i8,j8]TE[1] + ;1[i10,j10,k10,l10,m10]TE[1] + ) + ( + ;2[n11,o11,p11,o12,o13]TE[1] + ;3[n8,o8,n9,m10,n10]TE[1] + ;4[g7,h7,h8,i8,j8]TE[1] + ;1[i10,j10,k10,l10]TE[1] + ;2[l11,k12,l12,m12,m13]TE[1] + ;3[p9,q9,q10,r10,q11]TE[1] + ) + ) + ( + ;4[a1,a2,a3,b3,c3]TE[1] + ) + ( + ;4[a1,b1,b2,b3,c3]TE[1] + ;1[d14,e14,d15,c16,d16]TE[1] + ;2[p14,q14,q15,q16,r16]TE[1] + ;3[q5,r5,q6,p7,q7]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[g12,h12,f13,g13,g14]TE[1] + ;2[m11,n11,n12,o12,o13]TE[1] + ;3[o8,o9,m10,n10,o10]TE[1] + ;4[g7,h7,h8,i8,j8]TE[1] + ;1[j10,k10,l10,i11,j11]TE[1] + ;2[i12,j12,k12,l12,k13]TE[1] + ;3[k7,k8,j9,k9,l9]TE[1] + ) + ( + ;4[a1,a2,a3,a4,b4]TE[1] + ;1[e14,c15,d15,e15,c16]TE[1] + ;2[p14,q14,q15,r15,r16]TE[1] + ;3[r5,q6,r6,p7,q7]TE[1] + ;4[c5,d5,d6,d7,e7]TE[1] + ;1[g11,h11,f12,g12,f13]TE[1] + ;2[n11,m12,n12,n13,o13]TE[1] + ;3[o8,o9,p9,n10,o10]TE[1] + ;4[f8,g8,h8,h9,i9]TE[1] + ;1[i10,j10,k10,l10,m10]TE[1] + ;2[i11,j11,k11,l11,j12]TE[1] + ) + ) + ) + ( + ;2[r18,r19,s19,t19,t20]TE[1] + ;3[t1,t2,t3,s4,t4]TE[1] + ( + ;4[a1,b1,b2,b3,c3]TE[1] + ;1[d14,e14,d15,c16,d16]TE[1] + ;2[o15,p15,p16,q16,q17]TE[1] + ;3[r5,q6,r6,p7,q7]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[g11,h11,f12,g12,f13]TE[1] + ;2[m13,l14,m14,n14,m15]TE[1] + ;3[o8,m9,n9,o9,o10]TE[1] + ;4[g7,h7,h8,i8,j8]TE[1] + ;1[j9,k9,l9,i10,j10]TE[1] + ;2[m11,n11,o11,p11,n12]TE[1] + ;3[l5,k6,l6,l7,l8]TE[1] + ;4[j4,j5,k5,i6,j6]TE[1] + ) + ( + ;4[a1,b1,c1,d1,d2]TE[1] + ;1[d14,e14,c15,d15,c16]TE[1] + ;2[o15,p15,p16,q16,q17]TE[1] + ;3[q5,r5,q6,p7,q7]TE[1] + ;4[e3,e4,f4,f5,g5]TE[1] + ;1[g12,h12,f13,g13,g14]TE[1] + ;2[m13,l14,m14,n14,m15]TE[1] + ;3[o8,o9,m10,n10,o10]TE[1] + ;4[h6,i6,i7,j7,k7]TE[1] + ;1[j10,k10,l10,i11,j11]TE[1] + ;2[l11,m11,n11,o11,n12]TE[1] + ;3[p11,q11,q12,r12,q13]TE[1] + ;4[m7,l8,m8,n8,l9]TE[1] + ) + ) + ( + ;2[r18,s18,s19,s20,t20]TE[1] + ;3[t1,t2,t3,s4,t4]TE[1] + ;4[a1,a2,b2,c2,c3]TE[1] + ( + ;1[e14,c15,d15,e15,c16]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[r5,q6,r6,p7,q7]TE[1] + ;4[d4,d5,d6,e6,e7]TE[1] + ;1[g11,h11,f12,g12,f13]TE[1] + ;2[n12,o12,m13,n13,n14]TE[1] + ;3[o8,o9,m10,n10,o10]TE[1] + ;4[f8,f9,g9,h9,i9]TE[1] + ;1[e8,d9,e9,e10,e11]TE[1] + ;2[l9,m9,l10,l11,m11]TE[1] + ;3[p11,p12,o13,p13,o14]TE[1] + ;4[c7,b8,c8,c9,c10]TE[1] + ) + ( + ;1[d14,e14,d15,c16,d16]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[q5,r5,q6,p7,q7]TE[1] + ;4[d4,d5,e5,e6,f6]TE[1] + ;1[g11,g12,h12,f13,g13]TE[1] + ;2[n12,o12,m13,n13,n14]TE[1] + ;3[o8,n9,o9,m10,n10]TE[1] + ;4[g7,h7,h8,i8,j8]TE[1] + ;1[j10,k10,l10,i11,j11]TE[1] + ;2[i12,j12,k12,l12,i13]TE[1] + ;3[l6,l7,k8,l8,l9]TE[1] + ;4[j4,j5,i6,j6,k6]TE[1] + ) + ) +) +( + ;1[d19,a20,b20,c20,d20]TE[1] + ;2[r18,s18,s19,s20,t20]TE[1] + ( + ;3[q1,r1,s1,t1,q2]TE[1] + ;4[a1,b1,b2,b3,c3]TE[1] + ;1[f16,g16,e17,f17,e18]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[p3,o4,p4,n5,o5]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[j13,h14,i14,j14,h15]TE[1] + ;2[m11,m12,m13,n13,n14]TE[1] + ;3[l6,m6,k7,l7,l8]TE[1] + ;4[g7,f8,g8,h8,h9]TE[1] + ;1[l9,l10,k11,l11,k12]TE[1] + ;2[m8,m9,n9,o9,n10]TE[1] + ;3[k9,j10,k10,j11,j12]TE[1] + ;4[i10,i11,i12,h13,i13]TE[1] + ) + ( + ;3[t1,t2,t3,s4,t4]TE[1] + ;4[a1,b1,b2,b3,c3]TE[1] + ;1[g16,e17,f17,g17,e18]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[q5,r5,q6,p7,q7]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[j13,i14,j14,h15,i15]TE[1] + ;2[m11,m12,m13,n13,n14]TE[1] + ) +) +( + ;1[b18,c18,b19,a20,b20]TE[1] + ( + ;2[r18,s18,s19,s20,t20]TE[1] + ( + ;3[s1,t1,s2,r3,s3]TE[1] + ( + ;4[a1,a2,b2,c2,c3]TE[1] + ;1[f16,g16,d17,e17,f17]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ( + ;3[o4,p4,q4,n5,o5]TE[1] + ;4[d4,d5,d6,e6,e7]TE[1] + ( + ;1[j13,i14,j14,h15,i15]TE[1] + ( + ;2[m11,m12,m13,n13,n14]TE[1] + ( + ;3[l6,m6,k7,l7,k8]TE[1] + ;4[f8,f9,g9,h9,g10]TE[1] + ;1[k9,k10,k11,k12]TE[1] + ( + ;2[l8,m8,n8,l9,l10]TE[1] + ;3[j9,j10,j11,j12]TE[1] + ;4[h11,g12,h12,h13,h14]TE[1] + ) + ( + ;2[l8,l9,m9,l10]TE[1] + ;3[i9,j9,j10,j11,j12]TE[1] + ;4[h11,g12,h12,h13,h14]TE[1] + ) + ) + ( + ;3[l5,k6,l6,m6,k7]TE[1] + ;4[f8,f9,g9,h9,g10]TE[1] + ;1[k8,k9,k10,k11,k12]TE[1] + ;2[l7,l8,m8,l9,l10]TE[1] + ;3[j8,j9,j10,j11,j12]TE[1] + ;4[h11,i11,h12,i12,i13]TE[1] + ) + ) + ( + ;2[k13,l13,m13,m14,n14]TE[1] + ( + ;3[l5,k6,l6,m6,k7]TE[1] + ( + ;4[f8,e9,f9,g9,g10]TE[1] + ;1[k8,k9,k10,k11,k12]TE[1] + ;2[o10,m11,n11,o11,n12]TE[1] + ;3[j8,j9,j10,j11,j12]TE[1] + ;4[h11,g12,h12,h13,h14]TE[1] + ) + ( + ;4[h7,f8,g8,h8,h9]TE[1] + ;1[k8,k9,k10,k11,k12]TE[1] + ;2[n9,n10,o10,n11,n12]TE[1] + ;3[j8,j9,j10,j11,j12]TE[1] + ;4[i10,i11,h12,i12,i13]TE[1] + ) + ) + ( + ;3[l6,m6,k7,l7,k8]TE[1] + ;4[f8,f9,g9,h9,g10]TE[1] + ;1[k9,k10,k11,k12]TE[1] + ;2[i9,j9,j10,j11,j12]TE[1] + ;3[j4,i5,j5,k5,j6]TE[1] + ;4[f4,f5,g5,g6,h6]TE[1] + ) + ) + ) + ( + ;1[j14,h15,i15,j15,j16]TE[1] + ;2[m11,m12,m13,n13,n14]TE[1] + ;3[l6,m6,k7,l7,k8]TE[1] + ;4[f8,f9,g9,h9,g10]TE[1] + ;1[k9,k10,k11,k12,k13]TE[1] + ;2[l8,m8,l9,m9,l10]TE[1] + ;3[j9,j10,j11,j12,j13]TE[1] + ;4[h11,h12,g13,h13,h14]TE[1] + ) + ( + ;1[j14,h15,i15,j15,i16]TE[1] + ;2[k13,l13,m13,m14,n14]TE[1] + ;3[m6,l7,m7,n7,m8]TE[1] + ;4[f8,f9,g9,h9,g10]TE[1] + ;1[i11,h12,i12,j12,i13]TE[1] + ;2[n10,m11,n11,o11,n12]TE[1] + ;3[n9,o9,o10,p10,p11]TE[1] + ;4[k9,i10,j10,k10,k11]TE[1] + ) + ) + ( + ;3[n4,o4,p4,q4,n5]TE[1] + ;4[d4,d5,d6,e6,e7]TE[1] + ;1[j13,i14,j14,h15,i15]TE[1] + ;2[m11,m12,m13,n13,n14]TE[1] + ;3[l5,k6,l6,m6,k7]TE[1] + ;4[f8,f9,g9,h9,g10]TE[1] + ;1[k8,k9,k10,k11,k12]TE[1] + ;2[l7,l8,m8,l9,l10]TE[1] + ;3[j8,j9,j10,j11,j12]TE[1] + ;4[h11,h12,g13,h13,h14]TE[1] + ) + ) + ( + ;4[a1,b1,b2,b3,c3]TE[1] + ;1[f16,g16,d17,e17,f17]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[o4,p4,q4,n5,o5]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ( + ;1[j14,h15,i15,j15,j16]TE[1] + ;2[k13,l13,l14,m14,n14]TE[1] + ;3[l6,m6,k7,l7,l8]TE[1] + ;4[g7,h7,h8,i8,h9]TE[1] + ;1[j11,i12,j12,h13,i13]TE[1] + ;2[m10,l11,m11,m12,n12]TE[1] + ;3[h5,g6,h6,i6,j6]TE[1] + ;4[g10,e11,f11,g11,g12]TE[1] + ) + ( + ;1[j14,h15,i15,j15,i16]TE[1] + ;2[k13,l13,m13,m14,n14]TE[1] + ;3[l6,m6,k7,l7,k8]TE[1] + ;4[g7,f8,g8,h8,g9]TE[1] + ;1[j11,i12,j12,h13,i13]TE[1] + ;2[n9,n10,n11,o11,n12]TE[1] + ;3[h5,g6,h6,i6,j6]TE[1] + ;4[i9,j9,k9,l9,m9]TE[1] + ) + ( + ;1[j13,i14,j14,h15,i15]TE[1] + ( + ;2[m11,m12,m13,n13,n14]TE[1] + ;3[l6,m6,k7,l7,k8]TE[1] + ( + ;4[g7,g8,h8,h9,h10]TE[1] + ;1[k9,k10,k11,k12]TE[1] + ;2[l8,l9,m9,l10]TE[1] + ;3[j9,j10,i11,j11,j12]TE[1] + ;4[g11,f12,g12,h12,h13]TE[1] + ) + ( + ;4[g7,f8,g8,h8,g9]TE[1] + ;1[k9,k10,k11,k12]TE[1] + ;2[l8,l9,m9,l10]TE[1] + ;3[j9,j10,j11,j12]TE[1] + ;4[h10,h11,g12,h12,h13]TE[1] + ) + ) + ( + ;2[k13,l13,l14,m14,n14]TE[1] + ;3[l6,m6,k7,l7,k8]TE[1] + ;4[g7,f8,g8,h8,g9]TE[1] + ;1[k9,k10,k11,l11,k12]TE[1] + ;2[j9,j10,i11,j11,j12]TE[1] + ;3[h5,g6,h6,i6,j6]TE[1] + ;4[g4,h4,i4,i5,j5]TE[1] + ) + ) + ) + ( + ;4[a1,a2,a3,b3,c3]TE[1] + ;1[f16,g16,d17,e17,f17]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[o4,p4,q4,n5,o5]TE[1] + ;4[d4,e4,e5,e6,f6]TE[1] + ;1[j13,i14,j14,h15,i15]TE[1] + ;2[k13,l13,l14,m14,n14]TE[1] + ;3[m6,l7,m7,n7,m8]TE[1] + ;4[h6,g7,h7,i7,i8]TE[1] + ;1[i10,h11,i11,j11,i12]TE[1] + ;2[l10,k11,l11,m11,m12]TE[1] + ;3[n9,n10,o10,p10,o11]TE[1] + ;4[l5,m5,j6,k6,l6]TE[1] + ) + ) + ( + ;3[t1,t2,t3,s4,t4]TE[1] + ( + ;4[a1,b1,b2,b3,c3]TE[1] + ;1[f15,e16,f16,d17,e17]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[q5,r5,q6,p7,q7]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[g12,h12,f13,g13,g14]TE[1] + ( + ;2[n12,o12,m13,n13,n14]TE[1] + ;3[o8,o9,m10,n10,o10]TE[1] + ;4[g7,g8,h8,i8,h9]TE[1] + ;1[j10,k10,l10,i11,j11]TE[1] + ;2[i12,j12,k12,l12,i13]TE[1] + ;3[p11,p12,o13,p13,o14]TE[1] + ;4[m8,j9,k9,l9,m9]TE[1] + ) + ( + ;2[o12,m13,n13,o13,n14]TE[1] + ;3[o8,o9,m10,n10,o10]TE[1] + ;4[g7,h7,h8,i8,j8]TE[1] + ;1[j10,k10,l10,i11,j11]TE[1] + ;2[i12,j12,k12,l12,i13]TE[1] + ;3[p11,p12,q12,r12,q13]TE[1] + ;4[k7,l7,m7,n7,o7]TE[1] + ) + ) + ( + ;4[a1,a2,b2,c2,c3]TE[1] + ;1[f15,e16,f16,d17,e17]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[q5,r5,p6,q6,p7]TE[1] + ;4[d4,d5,d6,e6,e7]TE[1] + ;1[g12,h12,f13,g13,g14]TE[1] + ;2[n12,o12,m13,n13,n14]TE[1] + ;3[o8,o9,m10,n10,o10]TE[1] + ;4[f8,f9,g9,h9,f10]TE[1] + ;1[j10,k10,l10,i11,j11]TE[1] + ;2[p8,p9,q9,p10,p11]TE[1] + ;3[k11,l11,i12,j12,k12]TE[1] + ;4[e11,e12,e13,e14,f14]TE[1] + ) + ) + ( + ;3[q1,r1,s1,t1,q2]TE[1] + ( + ;4[a1,b1,b2,b3,c3]TE[1] + ;1[f16,g16,d17,e17,f17]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ( + ;3[p3,o4,p4,n5,o5]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[j14,h15,i15,j15,i16]TE[1] + ;2[n12,m13,n13,o13,n14]TE[1] + ;3[m6,k7,l7,m7,k8]TE[1] + ;4[g7,h7,h8,i8,j8]TE[1] + ;1[k9,k10,k11,k12,k13]TE[1] + ;2[m8,m9,m10,l11,m11]TE[1] + ;3[j9,j10,j11,j12,j13]TE[1] + ;4[j5,i6,j6,k6,l6]TE[1] + ) + ( + ;3[o3,p3,o4,n5,o5]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[j14,h15,i15,j15,i16]TE[1] + ;2[k13,l13,l14,m14,n14]TE[1] + ;3[m6,l7,m7,n7,m8]TE[1] + ;4[g7,f8,g8,h8,g9]TE[1] + ;1[i11,h12,i12,j12,i13]TE[1] + ;2[m10,l11,m11,n11,m12]TE[1] + ;3[n9,n10,o10,p10,o11]TE[1] + ;4[f10,f11,g11,e12,f12]TE[1] + ) + ) + ( + ;4[a1,a2,b2,c2,c3]TE[1] + ;1[f16,g16,d17,e17,f17]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[o3,p3,o4,n5,o5]TE[1] + ;4[d4,d5,d6,e6,e7]TE[1] + ;1[j14,h15,i15,j15,i16]TE[1] + ;2[k13,l13,m13,m14,n14]TE[1] + ;3[m6,l7,m7,n7,m8]TE[1] + ;4[f8,f9,g9,h9,g10]TE[1] + ;1[j11,i12,j12,h13,i13]TE[1] + ;2[n10,m11,n11,o11,n12]TE[1] + ;3[i8,j8,k8,j9,j10]TE[1] + ;4[g6,i6,g7,h7,i7]TE[1] + ) + ) + ( + ;3[t1,t2,s3,t3,s4]TE[1] + ( + ;4[a1,b1,b2,b3,c3]TE[1] + ;1[f15,e16,f16,d17,e17]TE[1] + ( + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[r5,p6,q6,r6,p7]TE[1] + ( + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[g12,h12,f13,g13,g14]TE[1] + ;2[n12,o12,m13,n13,n14]TE[1] + ;3[o8,n9,o9,m10,n10]TE[1] + ;4[g7,g8,h8,i8,h9]TE[1] + ;1[j10,k10,l10,i11,j11]TE[1] + ;2[i12,j12,k12,l12,i13] + ;3[p10,p11,q11,p12,p13]TE[1] + ;4[l8,j9,k9,l9,m9]TE[1] + ) + ( + ;4[d4,d5,d6,e6,e7]TE[1] + ;1[g12,h12,f13,g13,g14]TE[1] + ;2[n12,o12,m13,n13,n14]TE[1] + ;3[o8,o9,m10,n10,o10]TE[1] + ) + ) + ( + ;2[o15,p15,p16,q16,q17]TE[1] + ;3[r5,p6,q6,r6,p7]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[g12,h12,f13,g13,g14]TE[1] + ;2[m13,l14,m14,n14,m15]TE[1] + ;3[o8,n9,o9,m10,n10]TE[1] + ;4[g7,f8,g8,h8,g9]TE[1] + ;1[j10,k10,l10,i11,j11]TE[1] + ;2[k11,l11,m11,n11,l12]TE[1] + ;3[p10,o11,p11,q11,p12]TE[1] + ;4[i9,j9,k9,l9,m9]TE[1] + ) + ) + ( + ;4[a1,b1,c1,d1,d2]TE[1] + ;1[f15,e16,f16,d17,e17]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[r5,p6,q6,r6,p7]TE[1] + ;4[e3,e4,f4,g4,g5]TE[1] + ;1[g12,h12,f13,g13,g14]TE[1] + ;2[n12,o12,m13,n13,n14]TE[1] + ;3[o8,o9,m10,n10,o10]TE[1] + ;4[h6,h7,i7,i8,j8]TE[1] + ;1[j10,k10,l10,i11,j11]TE[1] + ;2[p8,q8,p9,p10,p11]TE[1] + ;3[l11,l12,k13,l13,l14]TE[1] + ;4[h9,h10,f11,g11,h11]TE[1] + ) + ) + ) + ( + ;2[r18,r19,r20,s20,t20]TE[1] + ;3[s1,t1,s2,r3,s3]TE[1] + ;4[a1,b1,c1,c2,c3]TE[1] + ;1[f16,g16,d17,e17,f17]TE[1] + ;2[o15,o16,p16,q16,q17]TE[1] + ;3[o4,p4,q4,n5,o5]TE[1] + ;4[d4,d5,e5,f5,f6]TE[1] + ;1[j13,i14,j14,h15,i15]TE[1] + ;2[m11,m12,m13,n13,n14]TE[1] + ;3[l6,m6,k7,l7,k8]TE[1] + ;4[g7,f8,g8,h8,g9]TE[1] + ;1[k9,k10,k11,k12]TE[1] + ) + ( + ;2[s17,t17,t18,t19,t20] + ;3[q1,r1,s1,t1,q2]TE[1] + ;4[a1,a2,a3,a4,b4] + ;1[f16,g16,d17,e17,f17]TE[1] + ;2[p14,q14,q15,r15,r16] + ;3[o3,p3,n4,o4,n5]TE[1] + ;4[c5,d5,d6,d7,e7] + ;1[j14,h15,i15,j15,i16]TE[1] + ;2[m11,m12,n12,o12,o13] + ;3[m6,k7,l7,m7,k8]TE[1] + ;4[f8,g8,g9,h9,h10] + ;1[k9,k10,k11,k12,k13]TE[1] + ;2[l8,m8,n8,l9,l10] + ;3[j9,j10,j11,j12,j13]TE[1] + ) + ( + ;2[s17,s18,t18,t19,t20]TE[1] + ;3[s1,t1,s2,r3,s3]TE[1] + ;4[a1,b1,b2,b3,c3]TE[1] + ;1[f16,g16,d17,e17,f17]TE[1] + ;2[p14,p15,q15,q16,r16]TE[1] + ;3[o4,p4,q4,n5,o5]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[j14,h15,i15,j15,i16]TE[1] + ;2[m11,m12,n12,o12,o13]TE[1] + ;3[l6,m6,k7,l7,k8]TE[1] + ;4[g7,g8,h8,h9,h10]TE[1] + ;1[k9,k10,k11,k12,k13]TE[1] + ;2[n6,n7,n8,n9,n10]TE[1] + ;3[j9,j10,j11,j12,j13]TE[1] + ;4[i11,i12,i13,h14,i14]TE[1] + ) +) +( + ;1[a20,b20,c20,d20,e20]TE[1] + ( + ;2[s17,t17,t18,t19,t20]TE[1] + ;3[s1,t1,s2,r3,s3]TE[1] + ;4[a1,a2,a3,b3,c3]TE[1] + ;1[h17,g18,h18,f19,g19]TE[1] + ;2[p14,q14,q15,q16,r16]TE[1] + ;3[o4,p4,q4,n5,o5]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[i13,i14,h15,i15,i16]TE[1] + ;2[m11,m12,n12,n13,o13]TE[1] + ;3[l6,m6,k7,l7,k8]TE[1] + ;4[g7,g8,h8,h9,h10]TE[1] + ;1[g10,g11,h11,i11,h12]TE[1] + ;2[j10,k10,l10,k11,k12]TE[1] + ;3[h5,g6,h6,i6,j6]TE[1] + ;4[f9,e10,f10,f11,f12]TE[1] + ) + ( + ;2[r18,s18,s19,s20,t20]TE[1] + ;3[s1,t1,s2,r3,s3]TE[1] + ( + ;4[a1,a2,b2,c2,c3]TE[1] + ;1[h17,g18,h18,f19,g19]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[o4,p4,q4,n5,o5]TE[1] + ;4[d4,d5,d6,e6,e7]TE[1] + ;1[j13,j14,i15,j15,i16]TE[1] + ;2[m11,m12,m13,n13,n14]TE[1] + ;3[l6,m6,k7,l7,k8]TE[1] + ;4[f8,f9,g9,h9,g10]TE[1] + ;1[k9,k10,k11,l11,k12]TE[1] + ;2[m8,m9,n9,o9,n10]TE[1] + ;3[i9,j9,j10,j11,j12]TE[1] + ;4[j5,j6,j7,i8,j8]TE[1] + ) + ( + ;4[a1,b1,b2,b3,c3]TE[1] + ;1[h17,g18,h18,f19,g19]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[o4,p4,q4,n5,o5]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[i13,i14,h15,i15,i16]TE[1] + ;2[m11,m12,m13,n13,n14]TE[1] + ;3[l6,m6,k7,l7,k8]TE[1] + ( + ;4[g7,f8,g8,h8,g9]TE[1] + ;1[k9,j10,k10,j11,j12]TE[1] + ;2[l8,m8,l9,m9,l10]TE[1] + ;3[i9,j9,i10,i11,i12]TE[1] + ;4[e9,e10,f10,f11,f12]TE[1] + ) + ( + ;4[g7,g8,h8,h9,h10]TE[1] + ;1[k9,j10,k10,j11,j12]TE[1] + ;2[l8,l9,m9,l10]TE[1] + ;3[i9,j9,i10,i11,i12]TE[1] + ;4[g11,f12,g12,h12,h13]TE[1] + ) + ) + ) +) +( + ;1[a16,a17,a18,a19,a20]TE[1] + ;2[s17,t17,t18,t19,t20]TE[1] + ( + ;3[t1,t2,t3,t4,t5]TE[1] + ;4[a1,b1,c1,d1,d2]TE[1] + ;1[c13,d13,b14,c14,b15]TE[1] + ;2[p14,q14,q15,q16,r16]TE[1] + ;3[s6,r7,s7,q8,r8]TE[1] + ;4[e3,e4,f4,g4,g5]TE[1] + ;1[f11,g11,h11,e12,f12]TE[1] + ;2[m11,n11,n12,o12,o13]TE[1] + ;3[o9,p9,m10,n10,o10]TE[1] + ;4[h6,h7,i7,i8,j8]TE[1] + ;1[k9,i10,j10,k10,l10]TE[1] + ;2[i11,j11,k11,k12,l12]TE[1] + ;3[p11,p12,p13,q13,r13]TE[1] + ;4[k7,l7,l8,m8,n8]TE[1] + ) + ( + ;3[p1,q1,r1,s1,t1]TE[1] + ;4[a1,b1,b2,b3,c3]TE[1] + ;1[c13,d13,b14,c14,b15]TE[1] + ;2[p14,q14,q15,q16,r16]TE[1] + ;3[n2,o2,n3,m4,n4]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[f11,g11,h11,e12,f12]TE[1] + ;2[m11,m12,n12,n13,o13]TE[1] + ;3[k5,l5,j6,k6,k7]TE[1] + ;4[g7,g8,f9,g9,h9]TE[1] + ;1[k11,i12,j12,k12,k13]TE[1] + ;2[i9,j9,k9,k10,l10]TE[1] + ;3[l8,l9,m9,m10,n10]TE[1] + ;4[e10,c11,d11,e11,d12]TE[1] + ) +) +( + ;1[a18,b18,c18,a19,a20]TE[1] + ( + ;2[s17,t17,t18,t19,t20]TE[1] + ( + ;3[q1,r1,s1,t1,q2]TE[1] + ;4[a1,b1,b2,b3,c3]TE[1] + ;1[f16,g16,d17,e17,f17]TE[1] + ;2[p14,q14,q15,q16,r16]TE[1] + ;3[p3,o4,p4,n5,o5]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[j14,h15,i15,j15,j16]TE[1] + ;2[n12,m13,n13,o13,n14]TE[1] + ;3[k6,l6,m6,k7,k8]TE[1] + ;4[g7,f8,g8,h8,g9]TE[1] + ;1[k9,k10,k11,k12,k13]TE[1] + ;2[l8,l9,l10,m10,m11]TE[1] + ;3[j9,j10,j11,j12,j13]TE[1] + ;4[h10,h11,h12,h13,h14]TE[1] + ) + ( + ;3[s1,t1,s2,r3,s3]TE[1] + ;4[a1,a2,b2,c2,c3]TE[1] + ;1[e15,f15,e16,d17,e17]TE[1] + ;2[p14,q14,q15,q16,r16]TE[1] + ;3[o4,p4,q4,n5,o5]TE[1] + ;4[d4,d5,d6,e6,e7]TE[1] + ;1[i13,g14,h14,i14,h15]TE[1] + ;2[m11,m12,n12,n13,o13]TE[1] + ;3[l6,m6,l7,l8,l9]TE[1] + ;4[f8,f9,g9,h9,g10]TE[1] + ;1[l10,k11,l11,j12,k12]TE[1] + ;2[m8,m9,n9,o9,n10]TE[1] + ;3[j10,k10,i11,j11,i12]TE[1] + ;4[f11,f12,e13,f13,f14]TE[1] + ) + ) + ( + ;2[r18,s18,s19,s20,t20]TE[1] + ( + ;3[q1,r1,s1,t1,q2]TE[1] + ;4[a1,a2,a3,b3,c3]TE[1] + ;1[e15,f15,e16,d17,e17]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[p3,n4,o4,p4,n5]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[j13,g14,h14,i14,j14]TE[1] + ;2[n12,m13,n13,o13,n14]TE[1] + ;3[l6,m6,k7,l7,k8]TE[1] + ;4[g7,f8,g8,h8,g9]TE[1] + ;1[k9,k10,k11,k12]TE[1] + ;2[l8,l9,l10,m10,m11]TE[1] + ;3[j9,i10,j10,j11,j12]TE[1] + ;4[j6,k6,i7,j7,j8]TE[1] + ) + ( + ;3[t1,t2,r3,s3,t3]TE[1] + ;4[a1,b1,b2,b3,c3]TE[1] + ;1[e15,f15,e16,d17,e17]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[p4,q4,p5,o6,p6]TE[1] + ;4[d4,e4,e5,f5,f6]TE[1] + ;1[h12,i12,g13,h13,g14]TE[1] + ;2[m11,m12,m13,n13,n14]TE[1] + ;3[n7,n8,n9,m10,n10]TE[1] + ;4[g7,g8,h8,i8,h9]TE[1] + ;1[j10,k10,l10,j11,k11]TE[1] + ) + ) +) +( + ;1[c18,c19,a20,b20,c20]TE[1] + ;2[s17,t17,t18,t19,t20]TE[1] + ;3[t1,t2,t3,s4,t4]TE[1] + ;4[a1,b1,c1,d1,d2]TE[1] + ;1[f15,d16,e16,f16,d17]TE[1] + ;2[p14,q14,q15,q16,r16]TE[1] + ;3[q5,r5,q6,p7,q7]TE[1] + ;4[e3,e4,f4,g4,g5]TE[1] + ;1[i13,g14,h14,i14,i15]TE[1] + ;2[o11,p11,n12,o12,o13]TE[1] + ;3[k8,l8,m8,n8,o8]TE[1] + ;4[h6,i6,i7,j7,j8]TE[1] + ;1[k11,j12,k12,l12,k13]TE[1] + ;2[p8,p9,q9,q10,r10]TE[1] + ;3[j9,i10,j10,i11,i12]TE[1] + ;4[h8,g9,h9,h10,h11]TE[1] +) +( + ;1[c18,a19,b19,c19,a20]TE[1] + ;2[r18,s18,s19,s20,t20]TE[1] + ;3[t1,r2,s2,t2,r3]TE[1] + ;4[a1,b1,c1,d1,d2]TE[1] + ;1[f16,g16,d17,e17,f17]TE[1] + ;2[o15,o16,p16,p17,q17]TE[1] + ;3[o4,p4,q4,n5,o5]TE[1] + ;4[e3,f3,f4,g4,g5]TE[1] + ;1[j14,h15,i15,j15,i16]TE[1] + ;2[k13,l13,m13,m14,n14]TE[1] + ;3[m6,l7,m7,n7,m8]TE[1] + ;4[h6,g7,h7,i7,h8]TE[1] + ;1[i11,h12,i12,j12,i13]TE[1] + ;2[k15,l15,j16,k16,k17]TE[1] + ;3[n9,n10,o10,p10,n11]TE[1] + ;4[i9,i10,j10,k10,k11]TE[1] +) +) diff --git a/src/books/book_classic_3.blksgf b/src/books/book_classic_3.blksgf new file mode 100644 index 0000000..273d56e --- /dev/null +++ b/src/books/book_classic_3.blksgf @@ -0,0 +1,14 @@ +( +;GM[Blokus Three-Player] +( + ;1[a17,b17,a18,a19,a20]TE[1] + ;2[s17,t17,t18,t19,t20]TE[1] + ;3[t1,t2,t3,s4,t4]TE[1] +) +( + ;1[b17,a18,b18,a19,a20]TE[1] +) +( + ;1[a20,b20,c20,c19,c18]TE[1] +) +) diff --git a/src/books/book_duo.blksgf b/src/books/book_duo.blksgf new file mode 100644 index 0000000..6559619 --- /dev/null +++ b/src/books/book_duo.blksgf @@ -0,0 +1,212 @@ +( +;GM[Blokus Duo] +( + ;B[f9,e10,f10,g10,f11]TE[1] + ( + ;W[i4,h5,i5,j5,i6]TE[1] + ( + ;B[h7,g8,h8,h9,i9]TE[1] + ( + ;W[f5,e6,f6,g6,e7]TE[1] + ) + ( + ;W[f5,f6,g6,e7,f7]TE[1] + ) + ) + ( + ;B[g7,g8,h8,i8,h9]TE[1] + ) + ( + ;B[e7,c8,d8,e8,d9]TE[1] + ( + ;W[h7,h8,i8,h9,h10]TE[1] + ) + ( + ;W[e5,d6,e6,f6,g6]TE[1] + ) + ( + ;W[h7,h8,h9,i9,h10]TE[1] + ) + ) + ( + ;B[e6,e7,d8,e8,d9]TE[1] + ) + ( + ;B[g6,g7,h7,i7,g8]TE[1] + ( + ;W[k6,j7,k7,j8,j9]TE[1] + ) + ( + ;W[f4,g4,e5,f5,e6]TE[1] + ) + ) + ( + ;B[h6,h7,i7,g8,h8]TE[1] + ) + ( + ;B[g6,g7,g8,h8,h9] + ;W[j7,j8,j9,k9,j10]TE[1] + ;B[h3,f4,g4,h4,f5]TE[1] + ;W[h10,h11,i11,g12,h12]TE[1] + ) + ( + ;B[g6,g7,g8,h8,i8] + ;W[f4,g4,e5,f5,e6]TE[1] + ) + ( + ;B[i7,g8,h8,i8,h9]BM[1] + ;W[g6,f7,g7,h7,f8]TE[1] + ) + ) + ( + ;W[j5,i6,j6,k6,j7]TE[1] + ) +) +( + ;B[e9,d10,e10,f10,e11]TE[1] + ;W[j4,i5,j5,k5,j6]TE[1] + ( + ;B[h6,g7,h7,f8,g8]TE[1] + ) + ( + ;B[h7,f8,g8,h8,g9]TE[1] + ) +) +( + ;B[f8,e9,f9,g9,e10]TE[1] + ;W[i4,h5,i5,j5,i6]TE[1] + ( + ;B[h8,i8,j8,k8,j9]TE[1] + ( + ;W[k6,k7,l7,l8,l9]TE[1] + ) + ( + ;W[f6,g6,f7,g7,g8]TE[1] + ) + ) + ( + ;B[g4,h4,g5,g6,g7]TE[1] + ) + ( + ;B[g5,g6,g7,h7,i7]TE[1] + ( + ;W[k6,j7,k7,k8,l8]TE[1] + ) + ( + ;W[k6,j7,k7,l7,j8]TE[1] + ) + ) + ( + ;B[g6,g7,h7,h8,i8] + ;W[j7,j8,i9,j9,i10]TE[1] + ) + ( + ;B[g4,g5,f6,g6,g7] + ;W[f3,g3,h3,e4,f4]TE[1] + ;B[i7,j7,k7,h8,i8] + ;W[k6,l6,l7,k8,l8]TE[1] + ) +) +( + ;B[e8,e9,f9,d10,e10]TE[1] + ;W[i4,h5,i5,j5,i6]TE[1] + ;B[g6,f7,g7,h7,g8]TE[1] + ;W[j7,j8,j9,k9,j10]TE[1] +) +( + ;B[e8,f8,d9,e9,e10]TE[1] + ( + ;W[j5,j6,k6,i7,j7]TE[1] + ( + ;B[g6,g7,h7,h8,i8]TE[1] + ( + ;W[i3,h4,i4,g5,h5]TE[1] + ) + ( + ;W[h4,i4,f5,g5,h5]TE[1] + ) + ) + ( + ;B[h5,h6,g7,h7,h8]TE[1] + ;W[f3,f4,g4,h4,i4]TE[1] + ) + ) + ( + ;W[j5,i6,j6,k6,j7]TE[1] + ;B[h5,i5,h6,g7,h7] + ( + ;W[g8,h8,i8,h9,i9]TE[1] + ) + ( + ;W[i8,i9,h10,i10,h11]TE[1] + ) + ) + ( + ;W[i4,h5,i5,j5,i6] + ;B[g4,g5,g6,g7,h7]TE[1] + ;W[g2,f3,g3,h3,f4] + ;B[k6,j7,k7,i8,j8]TE[1] + ) +) +( + ;B[f8,d9,e9,f9,e10]TE[1] + ;W[j5,i6,j6,k6,i7]TE[1] + ;B[h5,h6,g7,h7,h8]TE[1] + ( + ;W[g3,f4,g4,h4,i4]TE[1] + ) + ( + ;W[g4,h4,i4,f5,g5] + ;B[j8,k8,l8,i9,j9]TE[1] + ) +) +( + ;B[e8,d9,e9,e10,f10]TE[1] + ( + ;W[j5,i6,j6,k6,j7]TE[1] + ;B[f4,e5,f5,f6,f7]TE[1] + ( + ;W[f3,g3,g4,g5,h5]TE[1] + ) + ( + ;W[h7,h8,h9,i9,h10]TE[1] + ) + ( + ;W[g7,h7,f8,g8,f9]TE[1] + ) + ) + ( + ;W[i4,h5,i5,j5,i6]TE[1] + ;B[g4,g5,f6,g6,f7]TE[1] + ( + ;W[g2,f3,g3,h3,f4]TE[1] + ) + ( + ;W[g7,h7,f8,g8,f9]TE[1] + ) + ) +) +( + ;B[f7,f8,e9,f9,e10]TE[1] + ;W[i4,i5,j5,h6,i6]TE[1] + ;B[h4,f5,g5,h5,g6]TE[1] +) +( + ;B[e7,e8,d9,e9,e10] + ;W[j5,i6,j6,h7,i7]TE[1] + ;B[h4,f5,g5,h5,f6] + ;W[f7,f8,g8,f9,f10]TE[1] +) +( + ;B[g9,e10,f10,g10,g11] + ;W[i4,h5,i5,j5,i6]TE[1] + ;B[j6,h7,i7,j7,h8] + ;W[f6,g6,f7,g7,g8]TE[1] +) +( + ;B[e10,f10,g10,h10,g11] + ;W[i4,h5,i5,j5,i6]TE[1] + ;B[j6,i7,j7,i8,i9] + ;W[e5,e6,f6,g6,g7]TE[1] +) +) diff --git a/src/books/book_junior.blksgf b/src/books/book_junior.blksgf new file mode 100644 index 0000000..9d86c8e --- /dev/null +++ b/src/books/book_junior.blksgf @@ -0,0 +1,9 @@ +( +;GM[Blokus Junior] +( + ;B[f9,e10,f10,e11,f11]TE[1] +) +( + ;B[g9,d10,e10,f10,g10]TE[1] +) +) diff --git a/src/books/book_nexos.blksgf b/src/books/book_nexos.blksgf new file mode 100644 index 0000000..7793fc6 --- /dev/null +++ b/src/books/book_nexos.blksgf @@ -0,0 +1,15 @@ +( +;GM[Nexos] +( + ;1[g16,g18,f19,e20]TE[1] +) +( + ;1[h17,g18,g20,f21]TE[1] +) +( + ;1[h17,g18,f19,e20]TE[1] +) +( + ;1[g16,g18,g20,f21]TE[1] +) +) diff --git a/src/books/book_nexos_2.blksgf b/src/books/book_nexos_2.blksgf new file mode 100644 index 0000000..e535a52 --- /dev/null +++ b/src/books/book_nexos_2.blksgf @@ -0,0 +1,15 @@ +( +;GM[Nexos Two-Player] +( + ;1[g16,g18,f19,e20]TE[1] +) +( + ;1[h17,g18,g20,f21]TE[1] +) +( + ;1[h17,g18,f19,e20]TE[1] +) +( + ;1[g16,g18,g20,f21]TE[1] +) +) diff --git a/src/books/book_trigon.blksgf b/src/books/book_trigon.blksgf new file mode 100644 index 0000000..da0292e --- /dev/null +++ b/src/books/book_trigon.blksgf @@ -0,0 +1,9 @@ +( +;GM[Blokus Trigon] +( + ;1[r12,r13,s13,r14,s14,r15]TE[1] +) +( + ;1[t12,s13,t13,r14,s14,r15]TE[1] +) +) diff --git a/src/books/book_trigon_2.blksgf b/src/books/book_trigon_2.blksgf new file mode 100644 index 0000000..99b9f7e --- /dev/null +++ b/src/books/book_trigon_2.blksgf @@ -0,0 +1,25 @@ +( +;GM[Blokus Trigon Two-Player] +( + ;1[r12,r13,s13,r14,s14,r15]TE[1] + ( + ;2[r4,q5,r5,q6,r6,r7]TE[1] + ;3[j7,k7,l7,m7,m8,n8]TE[1] + ;4[v11,w11,w12,x12,y12,z12]TE[1] + ;1[n9,o9,o10,p10,p11,q11]BM[1] + ;2[j6,k6,l6,m6,n6,o6]TE[1] + ) + ( + ;2[r4,r5,s5,r6,s6,r7] + ) + ( + ;2[r4,q5,r5,p6,q6,p7]TE[1] + ) +) +( + ;1[t12,s13,t13,r14,s14,r15]TE[1] + ;2[r4,q5,r5,p6,q6,p7]TE[1] + ;3[j7,k7,l7,m7,n7,o7]TE[1] + ;4[u12,v12,w12,x12,y12,z12]TE[1] +) +) diff --git a/src/books/book_trigon_3.blksgf b/src/books/book_trigon_3.blksgf new file mode 100644 index 0000000..7de2ba6 --- /dev/null +++ b/src/books/book_trigon_3.blksgf @@ -0,0 +1,9 @@ +( +;GM[Blokus Trigon Three-Player] +( + ;1[p11,o12,p12,o13,p13,p14]TE[1] +) +( + ;1[r11,q12,r12,p13,q13,p14]TE[1] +) +) diff --git a/src/books/pentobi_books.qrc b/src/books/pentobi_books.qrc new file mode 100644 index 0000000..3adda93 --- /dev/null +++ b/src/books/pentobi_books.qrc @@ -0,0 +1,17 @@ + + + book_callisto.blksgf + book_callisto_2.blksgf + book_callisto_3.blksgf + book_classic.blksgf + book_classic_2.blksgf + book_classic_3.blksgf + book_duo.blksgf + book_junior.blksgf + book_nexos.blksgf + book_nexos_2.blksgf + book_trigon.blksgf + book_trigon_2.blksgf + book_trigon_3.blksgf + + diff --git a/src/convert/CMakeLists.txt b/src/convert/CMakeLists.txt new file mode 100644 index 0000000..20bddab --- /dev/null +++ b/src/convert/CMakeLists.txt @@ -0,0 +1,3 @@ +add_executable(convert Main.cpp) + +target_link_libraries(convert Qt5::Widgets) diff --git a/src/convert/Main.cpp b/src/convert/Main.cpp new file mode 100644 index 0000000..6ba8a50 --- /dev/null +++ b/src/convert/Main.cpp @@ -0,0 +1,54 @@ +//----------------------------------------------------------------------------- +/** @file convert/Main.cpp + Utility program for converting icons between image formats. + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include +#include +#include +#include +#include + +//----------------------------------------------------------------------------- + +int main(int argc, char* argv[]) +{ + QCoreApplication app(argc, argv); + try + { + QCommandLineParser parser; + QCommandLineOption optionHdpi("hdpi"); + parser.addOption(optionHdpi); + parser.process(app); + auto args = parser.positionalArguments(); + if (args.size() != 2) + throw QString("Need two arguments"); + auto in = args.at(0); + auto out = args.at(1); + QImageReader reader(in); + QImage image = reader.read(); + if (image.isNull()) + throw QString("%1: %2").arg(in, reader.errorString()); + if (parser.isSet(optionHdpi)) + { + QImageReader reader(in); + reader.setScaledSize(2 * image.size()); + image = reader.read(); + if (image.isNull()) + throw QString("%1: %2").arg(in, reader.errorString()); + } + QImageWriter writer(out); + if (! writer.write(image)) + throw QString("%1: %2").arg(out, writer.errorString()); + } + catch (const QString& msg) + { + std::cerr << msg.toLocal8Bit().constData() << '\n'; + return 1; + } + return 0; +} + +//----------------------------------------------------------------------------- diff --git a/src/doc_libboardgame.cpp b/src/doc_libboardgame.cpp new file mode 100644 index 0000000..0081b4d --- /dev/null +++ b/src/doc_libboardgame.cpp @@ -0,0 +1,76 @@ +/** + +@page libboardgame_doc_tags Tags used in documentation +This page defines attributes of documentation elements that are in +widespread use. For brevity, the documentation block contains only +a reference to the section of this page. + +@section libboardgame_avoid_stack_allocation Class size is large +The size of this class is large because it contains large members that are not +allocated on the heap to avoid dereferencing pointers for speed reasons. It +should be avoided to create instances of this class on the stack. + +@section libboardgame_doc_obj_ref_opt Object reference optimization +This class uses a reference to a certain object several times but does not +store the reference at construction time for memory and/or speed optimization. +The reference is passed as an argument to the functions that need it. The +class instance assumes (and might check with assertions) that the reference +always refers to the same object . + +@section libboardgame_doc_storesref Stores a reference +Used for parameters to indicate that the class will store a reference to the +parameter. The lifetime of the parameter must exceed the lifetime of the +constructed class. + +@section libboardgame_doc_threadsafe_after_construction Thread-safe after +construction +Used for classes that, that are thread-safe (w.r.t. different instances) after +construction. The constructor (and potentially also the destructor) is not +thread-safe, for example because it modifies non-const static class members. + + +@page libboardgame_doc_glossary Glossary +This page explains and defines terms used in the documentation. + +@section libboardgame_doc_gogui GoGui +Graphical interface for Go engines using GTP. Defines several GTP extension +commands. http://gogui.sf.net + +@section libboardgame_doc_gnugo GNU Go +GNU Go program http://www.gnu.org/s/gnugo/ + +@section libboardgame_doc_gtp GTP +Go Text Protocol http://www.lysator.liu.se/~gunnar/gtp/ + +@section libboardgame_doc_uct UCT +Upper Confidence bounds applied to Tree: a Monte-Carlo tree search algorithm +that applies bandit ideas to the move selection at tree nodes. +See @ref libboardgame_doc_kocsis_szepesvari_2006 + +@section libboardgame_doc_rave RAVE +Rapid Action Value Estimation: Keeps track of the value of a move averaged +over all simulations in the subtree of a node in which the move was played +by a player in a position following the node (inclusive). +See @ref libboardgame_doc_gelly_silver_2007 + +@section libboardgame_doc_sgf SGF +Smart Game Format http://www.red-bean.com/sgf/ + +@page libboardgame_doc_bibliography Bibliography +List of publications. + +@section libboardgame_doc_enz_2009 A Lock-free Multithreaded Monte-Carlo Tree Search Algorithm. +M. Enzenberger, M. Mueller. Advances in Computer Games 2009. +(PDF) + +@section libboardgame_doc_gelly_silver_2007 Combining Online and Offline Knowledge in UCT. +S. Gelly, D. Silver. Proceedings of the 24th international conference on Machine learning, pp. 273-280, 2007. +(PDF) + +@section libboardgame_doc_kocsis_szepesvari_2006 Bandit Based Monte-Carlo Planning +L. Kocsis, Cs. Szepesvári. Proceedings of the 17th European Conference on +Machine Learning, Springer-Verlag, Berlin, LNCS/LNAI 4212, September 18-22, +pp. 282-293, 2006 +(PDF) + +*/ diff --git a/src/doc_mainpage.cpp b/src/doc_mainpage.cpp new file mode 100644 index 0000000..978b7a1 --- /dev/null +++ b/src/doc_mainpage.cpp @@ -0,0 +1,72 @@ +/** @mainpage notitle + + @section mainpage_libboardgame LibBoardGame Modules + + The LibBoardGame modules contain code that is not specific to the board + game Blokus and could be reused for other projects: + + - libboardgame_gtp - + Implementation of the Go Text Protocol GTP (@ref libboardgame_doc_gtp) + - libboardgame_sys - + Platform-dependent functionality + - libboardgame_util - + General utilities not specific to board games + - libboardgame_sgf - + Implementation of the Smart Game Format (@ref libboardgame_doc_sgf) + - libboardgame_base - + Utility classes and functions specific to board games + - libboardgame_mcts - + Monte-Carlo tree search + - libboardgame_test - + Functionality for unit tests similar to Boost::Test + + @section mainpage_pentobi Pentobi Modules + + The Pentobi modules are specific to the board game Blokus: + + - libpentobi_base - + General Blokus-specific functionality + - libpentobi_mcts - + Blokus player based on Monte-Carlo tree search + - pentobi_gtp - + GTP interface to the player in libpentobi_mcts + - twogtp - + Tool for playing games between two GTP engines + (currently only supported on Linux/GCC) + + @section mainpage_gui Pentobi QWidgets GUI Modules + + The Pentobi QWidgets GUI modules implement a user interface based on + Qt/QWidgets and targeted at desktops. + They have a dependency on the following + Qt libraries: QtCore4, QtGui4. + They are currently used for the desktop versions of Pentobi. + They may become obsolete in the future, once the QML GUI Modules + (@ref mainpage_gui_qml) provide the same functionality. + + - convert - + Small helper program to convert SVG icons to bitmaps at build time + - libpentobi_gui - + GUI functionality that could be reused for other projects + - libpentobi_thumbnail - + Common functionality for file preview thumbnailers + - pentobi - + Main program that provides a GUI for the player in libpentobi_mcts + - pentobi_thumbnailer - + Generates file preview thumbnails for the + Gnome desktop + - pentobi_kde_thumbnailer - + Plugin for file preview thumbnails for the + KDE desktop + + @section mainpage_gui_qml Pentobi QML GUI Modules + + The Pentobi QML GUI modules implement a user interface based on + Qt Quick / QML. They currently support only a subset of the features + of the QWidgets-based GUI (@ref mainpage_gui) but provide fluid + animations and are usable on touch-screens. They are currently + used for the Android version of Pentobi. + + - pentobi_qml - + Main program that provides a GUI for the player in libpentobi_mcts +*/ diff --git a/src/libboardgame_base/CMakeLists.txt b/src/libboardgame_base/CMakeLists.txt new file mode 100644 index 0000000..d51513d --- /dev/null +++ b/src/libboardgame_base/CMakeLists.txt @@ -0,0 +1,22 @@ +add_library(boardgame_base STATIC + CoordPoint.h + CoordPoint.cpp + Engine.h + Engine.cpp + Geometry.h + GeometryUtil.h + Grid.h + Marker.h + Point.h + PointTransform.h + Rating.h + Rating.cpp + RectGeometry.h + RectTransform.h + RectTransform.cpp + StringRep.h + StringRep.cpp + Transform.h + Transform.cpp +) + diff --git a/src/libboardgame_base/CoordPoint.cpp b/src/libboardgame_base/CoordPoint.cpp new file mode 100644 index 0000000..da120fe --- /dev/null +++ b/src/libboardgame_base/CoordPoint.cpp @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/CoordPoint.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "CoordPoint.h" + +#include + +namespace libboardgame_base { + +//----------------------------------------------------------------------------- + +ostream& operator<<(ostream& out, const CoordPoint& p) +{ + if (! p.is_null()) + out << '(' << p.x << ',' << p.y << ')'; + else + out << "NULL"; + return out; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base diff --git a/src/libboardgame_base/CoordPoint.h b/src/libboardgame_base/CoordPoint.h new file mode 100644 index 0000000..6761272 --- /dev/null +++ b/src/libboardgame_base/CoordPoint.h @@ -0,0 +1,128 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/CoordPoint.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_BASE_COORD_POINT_H +#define LIBBOARDGAME_BASE_COORD_POINT_H + +#include +#include + +namespace libboardgame_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** %Point stored as x,y coordinates. */ +struct CoordPoint +{ + int x; + + int y; + + static bool is_onboard(int x, int y, unsigned width, unsigned height); + + static CoordPoint null(); + + CoordPoint() = default; + + CoordPoint(int x, int y); + + bool operator==(const CoordPoint& p) const; + + bool operator!=(const CoordPoint& p) const; + + bool operator<(const CoordPoint& p) const; + + CoordPoint operator+(const CoordPoint& p) const; + + CoordPoint operator-(const CoordPoint& p) const; + + CoordPoint& operator+=(const CoordPoint& p); + + CoordPoint& operator-=(const CoordPoint& p); + + bool is_null() const; + + bool is_onboard(unsigned width, unsigned height) const; +}; + +inline CoordPoint::CoordPoint(int x, int y) +{ + this->x = x; + this->y = y; +} + +inline bool CoordPoint::operator==(const CoordPoint& p) const +{ + return x == p.x && y == p.y; +} + +inline bool CoordPoint::operator<(const CoordPoint& p) const +{ + if (y != p.y) + return y < p.y; + return x < p.x; +} + +inline bool CoordPoint::operator!=(const CoordPoint& p) const +{ + return ! operator==(p); +} + +inline CoordPoint CoordPoint::operator+(const CoordPoint& p) const +{ + return CoordPoint(x + p.x, y + p.y); +} + +inline CoordPoint& CoordPoint::operator+=(const CoordPoint& p) +{ + *this = *this + p; + return *this; +} + +inline CoordPoint CoordPoint::operator-(const CoordPoint& p) const +{ + return CoordPoint(x - p.x, y - p.y); +} + +inline CoordPoint& CoordPoint::operator-=(const CoordPoint& p) +{ + *this = *this - p; + return *this; +} + +inline CoordPoint CoordPoint::null() +{ + return CoordPoint(numeric_limits::max(), numeric_limits::max()); +} + +inline bool CoordPoint::is_onboard(int x, int y, unsigned width, + unsigned height) +{ + return x >= 0 && x < static_cast(width) + && y >= 0 && y < static_cast(height); +} + +inline bool CoordPoint::is_onboard(unsigned width, unsigned height) const +{ + return is_onboard(x, y, width, height); +} + +inline bool CoordPoint::is_null() const +{ + return x == numeric_limits::max(); +} + +//----------------------------------------------------------------------------- + +ostream& operator<<(ostream& out, const CoordPoint& p); + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_COORD_POINT_H diff --git a/src/libboardgame_base/Engine.cpp b/src/libboardgame_base/Engine.cpp new file mode 100644 index 0000000..195f849 --- /dev/null +++ b/src/libboardgame_base/Engine.cpp @@ -0,0 +1,57 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/Engine.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Engine.h" + +#include "libboardgame_sys/CpuTime.h" +#include "libboardgame_util/Log.h" +#include "libboardgame_util/RandomGenerator.h" + +namespace libboardgame_base { + +using namespace std; +using libboardgame_gtp::Failure; +using libboardgame_util::flush_log; +using libboardgame_util::RandomGenerator; + +//----------------------------------------------------------------------------- + +Engine::Engine() +{ + add("cputime", &Engine::cmd_cputime); + add("set_random_seed", &Engine::cmd_set_random_seed); +} + +Engine::~Engine() = default; + +void Engine::cmd_cputime(Response& response) +{ + double time = libboardgame_sys::cpu_time(); + if (time == -1) + throw Failure("cannot determine cpu time"); + response << time; +} + +/** Set global random seed. + Compatible with @ref libboardgame_doc_gnugo
+ Arguments: random seed */ +void Engine::cmd_set_random_seed(const Arguments& args) +{ + RandomGenerator::set_global_seed(args.parse()); +} + +void Engine::on_handle_cmd_begin() +{ + flush_log(); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base diff --git a/src/libboardgame_base/Engine.h b/src/libboardgame_base/Engine.h new file mode 100644 index 0000000..4498260 --- /dev/null +++ b/src/libboardgame_base/Engine.h @@ -0,0 +1,38 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/Engine.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_BASE_ENGINE_H +#define LIBBOARDGAME_BASE_ENGINE_H + +#include "libboardgame_gtp/Engine.h" + +namespace libboardgame_base { + +using libboardgame_gtp::Arguments; +using libboardgame_gtp::Response; + +//----------------------------------------------------------------------------- + +class Engine + : public libboardgame_gtp::Engine +{ +public: + void cmd_cputime(Response&); + void cmd_set_random_seed(const Arguments&); + + Engine(); + + ~Engine(); + +protected: + void on_handle_cmd_begin() override; +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_ENGINE_H diff --git a/src/libboardgame_base/Geometry.h b/src/libboardgame_base/Geometry.h new file mode 100644 index 0000000..27bdc48 --- /dev/null +++ b/src/libboardgame_base/Geometry.h @@ -0,0 +1,343 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/Geometry.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_BASE_GEOMETRY_H +#define LIBBOARDGAME_BASE_GEOMETRY_H + +#include +#include +#include "CoordPoint.h" +#include "StringRep.h" +#include "libboardgame_util/ArrayList.h" + +namespace libboardgame_base { + +using namespace std; +using libboardgame_util::ArrayList; + +//----------------------------------------------------------------------------- + +/** %Geometry data of a board with a given size. + This class is a base class that uses virtual functions in its constructor + that allow to restrict the shape of the board to a subset of the rectangle + and/or to define different definitions of adjacent and diagonal neighbors + of a point for geometries that are not a regular rectangular grid. + @tparam P An instantiation of libboardgame_base::Point (or compatible + class) + @tparam S A class with functions to convert points from and to strings + depending on the string representation of points in the game. */ +template +class Geometry +{ +public: + typedef P Point; + + typedef typename Point::IntType IntType; + + /** On-board adjacent neighbors of a point. */ + typedef ArrayList AdjList; + + /** On-board diagonal neighbors of a point + Currently supports up to nine diagonal points as used on boards + for Blokus Trigon. */ + typedef ArrayList DiagList; + + /** Adjacent neighbors of a coordinate. */ + typedef ArrayList AdjCoordList; + + /** Diagonal neighbors of a coordinate. */ + typedef ArrayList DiagCoordList; + + class Iterator + { + public: + explicit Iterator(IntType i) { m_i = i; } + + bool operator==(Iterator it) const { return m_i == it.m_i; } + + bool operator!=(Iterator it) const { return m_i != it.m_i; } + + void operator++() { ++m_i; } + + Point operator*() const { return Point(m_i); } + + private: + IntType m_i; + }; + + virtual ~Geometry(); + + virtual AdjCoordList get_adj_coord(int x, int y) const = 0; + + virtual DiagCoordList get_diag_coord(int x, int y) const = 0; + + /** Return the point type if the board has different types of points. + For example, in the geometry used in Blokus Trigon, there are two + point types (0=upward triangle, 1=downward triangle); in a regular + rectangle, there is only one point type. By convention, 0 is the + type of the point at (0,0). + @param x The x coordinate (may be negative and/or outside the board). + @param y The y coordinate (may be negative and/or outside the board). */ + virtual unsigned get_point_type(int x, int y) const = 0; + + /** Get repeat interval for point types along the x axis. + If the board has different point types, the layout of the point types + repeats in this x interval. If the board has only one point type, + the function should return 1. */ + virtual unsigned get_period_x() const = 0; + + /** Get repeat interval for point types along the y axis. + @see get_period_x(). */ + virtual unsigned get_period_y() const = 0; + + Iterator begin() const { return Iterator(0); } + + Iterator end() const { return Iterator(get_range()); } + + unsigned get_point_type(CoordPoint p) const; + + unsigned get_point_type(Point p) const; + + bool is_onboard(unsigned x, unsigned y) const; + + bool is_onboard(CoordPoint p) const; + + /** Return the point at a given coordinate. + @pre x < get_width() + @pre y < get_height() + @return The point or Point::null() if this coordinates are + off-board. */ + Point get_point(unsigned x, unsigned y) const; + + unsigned get_width() const; + + unsigned get_height() const; + + /** Get range used for onboard points. */ + IntType get_range() const; + + unsigned get_x(Point p) const; + + unsigned get_y(Point p) const; + + bool from_string(const string& s, Point& p) const; + + const string& to_string(Point p) const; + + const AdjList& get_adj(Point p) const; + + const DiagList& get_diag(Point p) const; + +protected: + explicit Geometry(unique_ptr string_rep = + unique_ptr(new StdStringRep)); + + /** Initialize. + Subclasses must call this function in their constructors. */ + void init(unsigned width, unsigned height); + + /** Initialize on-board points. + This function is used in init() and allows the subclass to restrict the + on-board points to a subset of the on-board points of a rectangle to + support different board shapes. It will only be called with x and + y within the width and height of the geometry. */ + virtual bool init_is_onboard(unsigned x, unsigned y) const = 0; + +private: + AdjList m_adj[Point::range_onboard]; + + DiagList m_diag[Point::range_onboard]; + + IntType m_range; + + Point m_points[Point::max_width][Point::max_height]; + + unique_ptr m_string_rep; + + unsigned m_width; + + unsigned m_height; + + unsigned m_x[Point::range_onboard]; + + unsigned m_y[Point::range_onboard]; + + unsigned m_point_type[Point::range_onboard]; + + string m_string[Point::range]; + +#if LIBBOARDGAME_DEBUG + bool is_valid(Point p) const; +#endif +}; + + +template +Geometry

::Geometry(unique_ptr string_rep) + : m_string_rep(move(string_rep)) +{ } + +template +Geometry

::~Geometry() = default; + +template +bool Geometry

::from_string(const string& s, Point& p) const +{ + istringstream in(s); + unsigned x; + unsigned y; + if (m_string_rep->read(in, m_width, m_height, x, y) + && is_onboard(CoordPoint(x, y))) + { + p = get_point(x, y); + return true; + } + return false; +} + +template +inline auto Geometry

::get_adj(Point p) const -> const AdjList& +{ + LIBBOARDGAME_ASSERT(is_valid(p)); + return m_adj[p.to_int()]; +} + +template +inline auto Geometry

::get_diag(Point p) const -> const DiagList& +{ + LIBBOARDGAME_ASSERT(is_valid(p)); + return m_diag[p.to_int()]; +} + +template +inline unsigned Geometry

::get_height() const +{ + return m_height; +} + +template +inline auto Geometry

::get_point(unsigned x, unsigned y) const -> Point +{ + LIBBOARDGAME_ASSERT(x < m_width); + LIBBOARDGAME_ASSERT(y < m_height); + return m_points[x][y]; +} + +template +inline unsigned Geometry

::get_point_type(Point p) const +{ + LIBBOARDGAME_ASSERT(is_valid(p)); + return m_point_type[p.to_int()]; +} + +template +inline unsigned Geometry

::get_point_type(CoordPoint p) const +{ + return get_point_type(p.x, p.y); +} + +template +inline auto Geometry

::get_range() const -> IntType +{ + return m_range; +} + +template +inline unsigned Geometry

::get_width() const +{ + return m_width; +} + +template +inline unsigned Geometry

::get_x(Point p) const +{ + LIBBOARDGAME_ASSERT(is_valid(p)); + return m_x[p.to_int()]; +} + +template +inline unsigned Geometry

::get_y(Point p) const +{ + LIBBOARDGAME_ASSERT(is_valid(p)); + return m_y[p.to_int()]; +} + +template +void Geometry

::init(unsigned width, unsigned height) +{ + LIBBOARDGAME_ASSERT(width >= 1); + LIBBOARDGAME_ASSERT(height >= 1); + LIBBOARDGAME_ASSERT(width <= Point::max_width); + LIBBOARDGAME_ASSERT(height <= Point::max_height); + m_width = width; + m_height = height; + m_string[Point::null().to_int()] = "null"; + IntType n = 0; + ostringstream ostr; + for (unsigned y = 0; y < height; ++y) + for (unsigned x = 0; x < width; ++x) + if (init_is_onboard(x, y)) + { + m_points[x][y] = Point(n); + m_x[n] = x; + m_y[n] = y; + ostr.str(""); + m_string_rep->write(ostr, x, y, width, height); + m_string[n] = ostr.str(); + ++n; + } + else + m_points[x][y] = Point::null(); + m_range = n; + for (IntType i = 0; i < m_range; ++i) + { + Point p(i); + auto x = get_x(p); + auto y = get_y(p); + for (auto& p : get_adj_coord(x, y)) + if (is_onboard(p)) + m_adj[i].push_back(get_point(p.x, p.y)); + for (auto& p : get_diag_coord(x, y)) + if (is_onboard(p)) + m_diag[i].push_back(get_point(p.x, p.y)); + m_point_type[i] = get_point_type(x, y); + } +} + +template +inline bool Geometry

::is_onboard(unsigned x, unsigned y) const +{ + return ! get_point(x, y).is_null(); +} + +template +bool Geometry

::is_onboard(CoordPoint p) const +{ + return p.is_onboard(m_width, m_height) && is_onboard(p.x, p.y); +} + +#if LIBBOARDGAME_DEBUG + +template +inline bool Geometry

::is_valid(Point p) const +{ + return ! p.is_null() && p.to_int() < get_range(); +} + +#endif + +template +inline const string& Geometry

::to_string(Point p) const +{ + LIBBOARDGAME_ASSERT(p.to_int() < get_range()); + return m_string[p.to_int()]; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_GEOMETRY_H diff --git a/src/libboardgame_base/GeometryUtil.h b/src/libboardgame_base/GeometryUtil.h new file mode 100644 index 0000000..99ff8f2 --- /dev/null +++ b/src/libboardgame_base/GeometryUtil.h @@ -0,0 +1,89 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/GeometryUtil.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_BASE_GEOMETRY_UTIL_H +#define LIBBOARDGAME_BASE_GEOMETRY_UTIL_H + +#include "Geometry.h" + +namespace libboardgame_base { +namespace geometry_util { + +//----------------------------------------------------------------------------- + +/** Shift a list of points as close to the (0,0) point as possible. + This will minimize the minimum x and y coordinates. The function also + returns the width and height of the bounding box and the offset that was + subtracted from the points for the shifting. + @note This transformation does not preserve point types. If the original + list was compatible with the point types on the board, the new point type of + (0,0) will be Geometry::get_point_type(offset). + @tparam T An iterator over a container containing CoordPoint element. */ +template +void normalize_offset(T begin, T end, unsigned& width, unsigned& height, + CoordPoint& offset) +{ + int min_x = numeric_limits::max(); + int min_y = numeric_limits::max(); + int max_x = numeric_limits::min(); + int max_y = numeric_limits::min(); + for (auto i = begin; i != end; ++i) + { + if (i->x < min_x) + min_x = i->x; + if (i->x > max_x) + max_x = i->x; + if (i->y < min_y) + min_y = i->y; + if (i->y > max_y) + max_y = i->y; + } + width = max_x - min_x + 1; + height = max_y - min_y + 1; + offset = CoordPoint(min_x, min_y); + for (auto i = begin; i != end; ++i) + *i -= offset; +} + +/** Get an offset to shift points that are not compatible with the point types + used in the geometry. + The offset shifts points in a minimal positive direction to match the + types, x-direction is preferred. + @param geo + @param point_type The point type of (0, 0) of the coordinate system used by + the points. */ +template +CoordPoint type_match_offset(const Geometry

& geo, unsigned point_type) +{ + for (unsigned y = 0; y < geo.get_period_y(); ++y) + for (unsigned x = 0; x < geo.get_period_x(); ++x) + if (geo.get_point_type(x, y) == point_type) + return CoordPoint(x, y); + LIBBOARDGAME_ASSERT(false); + return CoordPoint(0, 0); +} + +/** Apply type_match_offset() to a list of points. + @tparam T An iterator over a container containing CoordPoint elements. + @param geo The geometry. + @param begin The beginning of the list of points. + @param end The end of the list of points. + @param point_type The point type of (0,0) in the list of points. */ +template +void type_match_shift(const Geometry

& geo, T begin, T end, + unsigned point_type) +{ + CoordPoint offset = type_match_offset(geo, point_type); + for (auto i = begin; i != end; ++i) + *i += offset; +} + +//----------------------------------------------------------------------------- + +} // namespace geometry_util +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_GEOMETRY_UTIL_H diff --git a/src/libboardgame_base/Grid.h b/src/libboardgame_base/Grid.h new file mode 100644 index 0000000..1e1edd1 --- /dev/null +++ b/src/libboardgame_base/Grid.h @@ -0,0 +1,226 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/Grid.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_BASE_GRID_H +#define LIBBOARDGAME_BASE_GRID_H + +#include +#include +#include +#include +#include +#include "Geometry.h" + +namespace libboardgame_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +template +string grid_to_string(const T& grid, const Geometry& geo) +{ + ostringstream buffer; + size_t max_len = 0; + for (auto p : geo) + { + buffer.str(""); + buffer << grid[p]; + max_len = max(max_len, buffer.str().length()); + } + buffer.str(""); + auto width = geo.get_width(); + auto height = geo.get_height(); + string empty(max_len, ' '); + for (unsigned y = 0; y < height; ++y) + { + for (unsigned x = 0; x < width; ++x) + { + auto p = geo.get_point(x, y); + if (! p.is_null()) + buffer << setw(int(max_len)) << grid[p]; + else + buffer << empty; + if (x < width - 1) + buffer << ' '; + } + buffer << '\n'; + } + return buffer.str(); +} + +//----------------------------------------------------------------------------- + +template class GridExt; + +/** Elements assigned to on-board points. + The elements must be default-constructible. This class is a POD if the + element type is a POD. + @tparam P An instantiation of libboardgame_base::Point (or compatible + class) + @tparam T The element type. */ +template +class Grid +{ + friend class GridExt; // for GridExt::copy_from(Grid) + +public: + typedef P Point; + + typedef libboardgame_base::Geometry

Geometry; + + T& operator[](const Point& p); + + const T& operator[](const Point& p) const; + + /** Fill all on-board points for a given geometry with a value. */ + void fill(const T& val, const Geometry& geo); + + /** Fill points with a value. */ + void fill_all(const T& val); + + string to_string(const Geometry& geo) const; + + void copy_from(const Grid& grid, const Geometry& geo); + + /** Specialized version for trivially copyable elements. + Can be used instead of copy_from if the compiler is not smart enough to + figure out that it can use memcpy. + @pre std::is_trivially_copyable::value */ + void memcpy_from(const Grid& grid, const Geometry& geo); + +private: + T m_a[Point::range_onboard]; +}; + +template +inline T& Grid::operator[](const Point& p) +{ + LIBBOARDGAME_ASSERT(! p.is_null()); + return m_a[p.to_int()]; +} + +template +inline const T& Grid::operator[](const Point& p) const +{ + LIBBOARDGAME_ASSERT(! p.is_null()); + return m_a[p.to_int()]; +} + +template +inline void Grid::copy_from(const Grid& grid, const Geometry& geo) +{ + copy(grid.m_a, grid.m_a + geo.get_range(), m_a); +} + +template +inline void Grid::fill(const T& val, const Geometry& geo) +{ + std::fill(m_a, m_a + geo.get_range(), val); +} + +template +inline void Grid::fill_all(const T& val) +{ + std::fill(m_a, m_a + Point::range_onboard, val); +} + +template +void Grid::memcpy_from(const Grid& grid, const Geometry& geo) +{ + // std::is_trivially_copyable is not available with GCC < 5 +#if ! (__GNUC__ && __GNUC__ < 5) + static_assert(is_trivially_copyable::value, ""); +#endif + memcpy(&m_a, grid.m_a, geo.get_range() * sizeof(T)); +} + +template +string Grid::to_string(const Geometry& geo) const +{ + return grid_to_string(*this, geo); +} + +//----------------------------------------------------------------------------- + +/** Like Grid, but allows Point::null() as index. */ +template +class GridExt +{ +public: + typedef P Point; + + typedef libboardgame_base::Geometry

Geometry; + + T& operator[](const Point& p); + + const T& operator[](const Point& p) const; + + /** Fill all on-board points for a given geometry with a value. */ + void fill(const T& val, const Geometry& geo); + + /** Fill points with a value. */ + void fill_all(const T& val); + + string to_string(const Geometry& geo) const; + + void copy_from(const Grid& grid, const Geometry& geo); + + void copy_from(const GridExt& grid, const Geometry& geo); + +private: + T m_a[Point::range]; +}; + +template +inline T& GridExt::operator[](const Point& p) +{ + return m_a[p.to_int()]; +} + +template +inline const T& GridExt::operator[](const Point& p) const +{ + return m_a[p.to_int()]; +} + +template +inline void GridExt::fill(const T& val, const Geometry& geo) +{ + std::fill(m_a, m_a + geo.get_range(), val); +} + +template +inline void GridExt::fill_all(const T& val) +{ + std::fill(m_a, m_a + Point::range, val); +} + +template +inline void GridExt::copy_from(const Grid& grid, + const Geometry& geo) +{ + copy(grid.m_a, grid.m_a + geo.get_range(), m_a); +} + +template +inline void GridExt::copy_from(const GridExt& grid, + const Geometry& geo) +{ + copy(grid.m_a, grid.m_a + geo.get_range(), m_a); +} + +template +string GridExt::to_string(const Geometry& geo) const +{ + return grid_to_string(*this, geo); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_GRID_H diff --git a/src/libboardgame_base/Marker.h b/src/libboardgame_base/Marker.h new file mode 100644 index 0000000..591fb0a --- /dev/null +++ b/src/libboardgame_base/Marker.h @@ -0,0 +1,101 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/Marker.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_BASE_MARKER_H +#define LIBBOARDGAME_BASE_MARKER_H + +#include +#include + +namespace libboardgame_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** %Marker to mark points on board with fast operation to clear all marks. + This marker is typically used in recursive fills or other loops to + remember what points have already been visited. + @tparam P An instantiation of libboardgame_base::Point */ +template +class Marker +{ +public: + typedef P Point; + + Marker(); + + void clear(); + + /** Mark a point. + @return true if the point was already marked. */ + bool set(Point p); + + bool operator[](Point p) const; + + /** Set up for overflow test (for testing purposes only). + The function is equivalent to calling reset() and then clear() + nu_clear times. It allows a faster implementation of a unit test case + that tests if the overflow is handled correctly, if clear() is called + more than numeric_limits::max() times. */ + void setup_for_overflow_test(unsigned nu_clear); + +private: + unsigned m_current; + + unsigned m_a[Point::range]; + + void reset(); +}; + +template +inline Marker

::Marker() +{ + reset(); +} + +template +bool Marker

::operator[](Point p) const +{ + return m_a[p.to_int()] == m_current; +} + +template +inline void Marker

::clear() +{ + if (--m_current == 0) + reset(); +} + +template +inline void Marker

::setup_for_overflow_test(unsigned nu_clear) +{ + reset(); + m_current -= nu_clear; +} + +template +inline void Marker

::reset() +{ + m_current = numeric_limits::max() - 1; + fill(m_a, m_a + Point::range, numeric_limits::max()); +} + +template +inline bool Marker

::set(Point p) +{ + auto& a = m_a[p.to_int()]; + if (a == m_current) + return true; + a = m_current; + return false; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_MARKER_H diff --git a/src/libboardgame_base/Point.h b/src/libboardgame_base/Point.h new file mode 100644 index 0000000..f0e0a5e --- /dev/null +++ b/src/libboardgame_base/Point.h @@ -0,0 +1,149 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/Point.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_BASE_POINT_H +#define LIBBOARDGAME_BASE_POINT_H + +#include +#include "libboardgame_util/Assert.h" +#include "libboardgame_sys/Compiler.h" + +namespace libboardgame_base { + +using namespace std; +using namespace libboardgame_util; + +//----------------------------------------------------------------------------- + +/** Coordinate on the board. + Depending on the game, a point represents a field or intersection (in Go) + on the board. The class is a lightweight wrapper around an integer. All + information about points including the coordinates are contained in + Geometry. The convention for the coordinates is that the top left corner of + the board has the coordinates (0,0). Point::null() has the meaning + "no point". + @tparam M The maximum number of on-board points of all geometries this + point is used in (excluding the null point). + @tparam W The maximum width of all geometries this point is used in. + @tparam H The maximum height of all geometries this point is used in. + @tparam I An unsigned integer type to store the point value. */ +template +class Point +{ +public: + typedef I IntType; + + static const unsigned max_onboard = M; + + static const unsigned max_width = W; + + static const unsigned max_height = W; + + static_assert(numeric_limits::is_integer, ""); + + static_assert(! numeric_limits::is_signed, ""); + + static_assert(max_onboard <= max_width * max_height, ""); + + static const unsigned range_onboard = max_onboard; + + static const unsigned range = max_onboard + 1; + + static Point null(); + + LIBBOARDGAME_FORCE_INLINE Point(); + + explicit Point(unsigned i); + + bool operator==(const Point& p) const; + + bool operator!=(const Point& p) const; + + bool operator<(const Point& p) const; + + bool is_null() const; + + /** Return point as an integer between 0 and Point::range */ + unsigned to_int() const; + +private: + static const IntType value_uninitialized = range; + + static const IntType value_null = range - 1; + + IntType m_i; + + LIBBOARDGAME_FORCE_INLINE bool is_initialized() const; +}; + +template +inline Point::Point() +{ +#if LIBBOARDGAME_DEBUG + m_i = value_uninitialized; +#endif +} + +template +inline Point::Point(unsigned i) +{ + LIBBOARDGAME_ASSERT(i < range); + m_i = static_cast(i); +} + +template +inline bool Point::operator==(const Point& p) const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + LIBBOARDGAME_ASSERT(p.is_initialized()); + return m_i == p.m_i; +} + +template +inline bool Point::operator!=(const Point& p) const +{ + return ! operator==(p); +} + +template +inline bool Point::operator<(const Point& p) const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + LIBBOARDGAME_ASSERT(p.is_initialized()); + return m_i < p.m_i; +} + +template +inline bool Point::is_initialized() const +{ + return m_i < value_uninitialized; +} + +template +inline bool Point::is_null() const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + return m_i == value_null; +} + +template +inline auto Point::null() -> Point +{ + return Point(value_null); +} + +template +inline unsigned Point::to_int() const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + return m_i; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_POINT_H diff --git a/src/libboardgame_base/PointTransform.h b/src/libboardgame_base/PointTransform.h new file mode 100644 index 0000000..e86545c --- /dev/null +++ b/src/libboardgame_base/PointTransform.h @@ -0,0 +1,429 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/PointTransform.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_BASE_POINT_TRANSFORM_H +#define LIBBOARDGAME_BASE_POINT_TRANSFORM_H + +#include +#include "Geometry.h" +#include "libboardgame_util/Unused.h" + +namespace libboardgame_base { + +//----------------------------------------------------------------------------- + +/** %Transform a point. + @tparam P An instance of class Point. */ +template +class PointTransform +{ +public: + typedef P Point; + + virtual ~PointTransform(); + + virtual Point get_transformed(const Point& p, + const Geometry

& geo) const = 0; +}; + +template +PointTransform

::~PointTransform() = default; + +//----------------------------------------------------------------------------- + +template +class PointTransfIdent + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfIdent

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + LIBBOARDGAME_UNUSED(geo); + return p; +} + +//----------------------------------------------------------------------------- + +/** Rotate point by 90 degrees. */ +template +class PointTransfRot90 + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfRot90

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + unsigned x = geo.get_width() - geo.get_y(p) - 1; + unsigned y = geo.get_x(p); + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +/** Rotate point by 180 degrees. */ +template +class PointTransfRot180 + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfRot180

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + unsigned x = geo.get_width() - geo.get_x(p) - 1; + unsigned y = geo.get_height() - geo.get_y(p) - 1; + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +/** Rotate point by 270 degrees. */ +template +class PointTransfRot270 + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfRot270

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + unsigned x = geo.get_y(p); + unsigned y = geo.get_height() - geo.get_x(p) - 1; + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +/** Rotate point by 270 degrees and reflect on y axis shifted to the center. + This is equivalent to a reflection on the x=y line. */ +template +class PointTransfRot270Refl + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfRot270Refl

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + return geo.get_point(geo.get_y(p), geo.get_x(p)); +} + +//----------------------------------------------------------------------------- + +/** Rotate point by 90 degrees and reflect on y axis shifted to the center. + This is equivalent to a reflection on the x=width-y line. */ +template +class PointTransfRot90Refl + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfRot90Refl

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + unsigned x = geo.get_width() - geo.get_y(p) - 1; + unsigned y = geo.get_height() - geo.get_x(p) - 1; + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +/** Mirror along x axis. */ +template +class PointTransfRefl + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfRefl

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + unsigned x = geo.get_width() - geo.get_x(p) - 1; + unsigned y = geo.get_y(p); + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +/** Mirror along y axis. */ +template +class PointTransfReflRot180 + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfReflRot180

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + unsigned x = geo.get_x(p); + unsigned y = geo.get_height() - geo.get_y(p) - 1; + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +template +class PointTransfTrigonRot60 + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfTrigonRot60

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + float cx = 0.5f * static_cast(geo.get_width() - 1); + float cy = 0.5f * static_cast(geo.get_height() - 1); + float px = static_cast(geo.get_x(p)) - cx; + float py = static_cast(geo.get_y(p)) - cy; + unsigned x = static_cast(round(cx + 0.5f * px + 1.5f * py)); + unsigned y = static_cast(round(cy - 0.5f * px + 0.5f * py)); + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +template +class PointTransfTrigonRot120 + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfTrigonRot120

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + float cx = 0.5f * static_cast(geo.get_width() - 1); + float cy = 0.5f * static_cast(geo.get_height() - 1); + float px = static_cast(geo.get_x(p)) - cx; + float py = static_cast(geo.get_y(p)) - cy; + unsigned x = static_cast(round(cx - 0.5f * px + 1.5f * py)); + unsigned y = static_cast(round(cy - 0.5f * px - 0.5f * py)); + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +template +class PointTransfTrigonRot240 + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfTrigonRot240

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + float cx = 0.5f * static_cast(geo.get_width() - 1); + float cy = 0.5f * static_cast(geo.get_height() - 1); + float px = static_cast(geo.get_x(p)) - cx; + float py = static_cast(geo.get_y(p)) - cy; + unsigned x = static_cast(round(cx - 0.5f * px - 1.5f * py)); + unsigned y = static_cast(round(cy + 0.5f * px - 0.5f * py)); + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +template +class PointTransfTrigonRot300 + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfTrigonRot300

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + float cx = 0.5f * static_cast(geo.get_width() - 1); + float cy = 0.5f * static_cast(geo.get_height() - 1); + float px = static_cast(geo.get_x(p)) - cx; + float py = static_cast(geo.get_y(p)) - cy; + unsigned x = static_cast(round(cx + 0.5f * px - 1.5f * py)); + unsigned y = static_cast(round(cy + 0.5f * px + 0.5f * py)); + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +template +class PointTransfTrigonReflRot60 + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfTrigonReflRot60

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + float cx = 0.5f * static_cast(geo.get_width() - 1); + float cy = 0.5f * static_cast(geo.get_height() - 1); + float px = static_cast(geo.get_x(p)) - cx; + float py = static_cast(geo.get_y(p)) - cy; + unsigned x = static_cast(round(cx + 0.5f * (-px) + 1.5f * py)); + unsigned y = static_cast(round(cy - 0.5f * (-px) + 0.5f * py)); + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +template +class PointTransfTrigonReflRot120 + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfTrigonReflRot120

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + float cx = 0.5f * static_cast(geo.get_width() - 1); + float cy = 0.5f * static_cast(geo.get_height() - 1); + float px = static_cast(geo.get_x(p)) - cx; + float py = static_cast(geo.get_y(p)) - cy; + unsigned x = static_cast(round(cx - 0.5f * (-px) + 1.5f * py)); + unsigned y = static_cast(round(cy - 0.5f * (-px) - 0.5f * py)); + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +template +class PointTransfTrigonReflRot240 + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfTrigonReflRot240

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + float cx = 0.5f * static_cast(geo.get_width() - 1); + float cy = 0.5f * static_cast(geo.get_height() - 1); + float px = static_cast(geo.get_x(p)) - cx; + float py = static_cast(geo.get_y(p)) - cy; + unsigned x = static_cast(round(cx - 0.5f * (-px) - 1.5f * py)); + unsigned y = static_cast(round(cy + 0.5f * (-px) - 0.5f * py)); + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +template +class PointTransfTrigonReflRot300 + : public PointTransform

+{ +public: + typedef P Point; + + Point get_transformed(const Point& p, + const Geometry

& geo) const override; +}; + +template +P PointTransfTrigonReflRot300

::get_transformed(const Point& p, + const Geometry

& geo) const +{ + float cx = 0.5f * static_cast(geo.get_width() - 1); + float cy = 0.5f * static_cast(geo.get_height() - 1); + float px = static_cast(geo.get_x(p)) - cx; + float py = static_cast(geo.get_y(p)) - cy; + unsigned x = static_cast(round(cx + 0.5f * (-px) - 1.5f * py)); + unsigned y = static_cast(round(cy + 0.5f * (-px) + 0.5f * py)); + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_POINT_TRANSFORM_H diff --git a/src/libboardgame_base/Rating.cpp b/src/libboardgame_base/Rating.cpp new file mode 100644 index 0000000..9fd9136 --- /dev/null +++ b/src/libboardgame_base/Rating.cpp @@ -0,0 +1,52 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/Rating.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Rating.h" + +#include +#include +#include "libboardgame_util/Assert.h" + +namespace libboardgame_base { + +//----------------------------------------------------------------------------- + +ostream& operator<<(ostream& out, const Rating& rating) +{ + out << rating.m_elo; + return out; +} + +istream& operator>>(istream& in, Rating& rating) +{ + in >> rating.m_elo; + return in; +} + +float Rating::get_expected_result(Rating elo_opponent, + unsigned nu_opponents) const +{ + float diff = elo_opponent.m_elo - m_elo; + return + 1.f + / (1.f + static_cast(nu_opponents) * pow(10.f, diff / 400.f)); +} + +void Rating::update(float game_result, Rating elo_opponent, float k_value, + unsigned nu_opponents) +{ + LIBBOARDGAME_ASSERT(k_value > 0); + float diff = game_result - get_expected_result(elo_opponent, nu_opponents); + m_elo += k_value * diff; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base diff --git a/src/libboardgame_base/Rating.h b/src/libboardgame_base/Rating.h new file mode 100644 index 0000000..8b190a7 --- /dev/null +++ b/src/libboardgame_base/Rating.h @@ -0,0 +1,72 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/Rating.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_BASE_RATING_H +#define LIBBOARDGAME_BASE_RATING_H + +#include +#include + +namespace libboardgame_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Elo-rating of a player. */ +class Rating +{ +public: + friend ostream& operator<<(ostream& out, const Rating& rating); + friend istream& operator>>(istream& in, Rating& rating); + + explicit Rating(float elo = 0); + + /** Get the expected outcome of a game. + @param elo_opponent Elo-rating of the opponent. + @param nu_opponents The number of opponents (all with the same rating + elo_opponent) */ + float get_expected_result(Rating elo_opponent, + unsigned nu_opponents = 1) const; + + /** Update a rating after a game. + @param game_result The outcome of the game (0=loss, 0.5=tie, 1=win) + @param elo_opponent Elo-rating of the opponent. + @param k_value The K-value + @param nu_opponents The number of opponents (all with the same rating + elo_opponent) */ + void update(float game_result, Rating elo_opponent, float k_value = 32, + unsigned nu_opponents = 1); + + float get() const; + + /** Get rating rounded to an integer. */ + int to_int() const; + +private: + float m_elo; +}; + +inline Rating::Rating(float elo) + : m_elo(elo) +{ +} + +inline float Rating::get() const +{ + return m_elo; +} + +inline int Rating::to_int() const +{ + return static_cast(round(m_elo)); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_RATING_H diff --git a/src/libboardgame_base/RectGeometry.h b/src/libboardgame_base/RectGeometry.h new file mode 100644 index 0000000..ea47e0a --- /dev/null +++ b/src/libboardgame_base/RectGeometry.h @@ -0,0 +1,137 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/RectGeometry.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_BASE_RECT_GEOMETRY_H +#define LIBBOARDGAME_BASE_RECT_GEOMETRY_H + +#include +#include +#include "Geometry.h" +#include "libboardgame_util/Unused.h" + +namespace libboardgame_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Geometry of a regular rectangular grid. + @tparam P An instantiation of libboardgame_base::Point */ +template +class RectGeometry final + : public Geometry

+{ +public: + typedef P Point; + + using AdjCoordList = typename Geometry

::AdjCoordList; + using DiagCoordList = typename Geometry

::DiagCoordList; + using AdjList = typename Geometry

::AdjList; + using DiagList = typename Geometry

::DiagList; + + /** Create or reuse an already created geometry with a given size. */ + static const RectGeometry& get(unsigned width, unsigned height); + + RectGeometry(unsigned width, unsigned height); + + AdjCoordList get_adj_coord(int x, int y) const override; + + DiagCoordList get_diag_coord(int x, int y) const override; + + unsigned get_point_type(int x, int y) const override; + + unsigned get_period_x() const override; + + unsigned get_period_y() const override; + +protected: + bool init_is_onboard(unsigned x, unsigned y) const override; + +private: + /** Stores already created geometries by width and height. */ + static map, shared_ptr> s_geometry; +}; + +template +map, shared_ptr>> + RectGeometry

::s_geometry; + +template +RectGeometry

::RectGeometry(unsigned width, unsigned height) +{ + Geometry

::init(width, height); +} + +template +const RectGeometry

& RectGeometry

::get(unsigned width, unsigned height) +{ + auto key = make_pair(width, height); + auto pos = s_geometry.find(key); + if (pos != s_geometry.end()) + return *pos->second; + auto geometry = make_shared(width, height); + return *s_geometry.insert(make_pair(key, geometry)).first->second; +} + +template +auto RectGeometry

::get_adj_coord(int x, int y) const -> AdjCoordList +{ + AdjCoordList l; + l.push_back(CoordPoint(x, y - 1)); + l.push_back(CoordPoint(x - 1, y)); + l.push_back(CoordPoint(x + 1, y)); + l.push_back(CoordPoint(x, y + 1)); + return l; +} + +template +auto RectGeometry

::get_diag_coord(int x, int y) const -> DiagCoordList +{ + // The order does not matter logically but it is better to put far away + // points first because in Blokus, libpentobi::BoardConst uses the + // forbidden status of the first points during move generation and far away + // points can reject more moves. + DiagCoordList l; + l.push_back(CoordPoint(x - 1, y - 1)); + l.push_back(CoordPoint(x + 1, y + 1)); + l.push_back(CoordPoint(x + 1, y - 1)); + l.push_back(CoordPoint(x - 1, y + 1)); + return l; +} + +template +unsigned RectGeometry

::get_period_x() const +{ + return 1; +} + +template +unsigned RectGeometry

::get_period_y() const +{ + return 1; +} + +template +unsigned RectGeometry

::get_point_type(int x, int y) const +{ + LIBBOARDGAME_UNUSED(x); + LIBBOARDGAME_UNUSED(y); + return 0; +} + +template +bool RectGeometry

::init_is_onboard(unsigned x, unsigned y) const +{ + LIBBOARDGAME_UNUSED(x); + LIBBOARDGAME_UNUSED(y); + return true; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_RECT_GEOMETRY_H diff --git a/src/libboardgame_base/RectTransform.cpp b/src/libboardgame_base/RectTransform.cpp new file mode 100644 index 0000000..5553da4 --- /dev/null +++ b/src/libboardgame_base/RectTransform.cpp @@ -0,0 +1,73 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/RectTransform.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "RectTransform.h" + +namespace libboardgame_base { + +//----------------------------------------------------------------------------- + +CoordPoint TransfIdentity::get_transformed(const CoordPoint& p) const +{ + return p; +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfRectRot90::get_transformed(const CoordPoint& p) const +{ + return CoordPoint(-p.y, p.x); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfRectRot180::get_transformed(const CoordPoint& p) const +{ + return CoordPoint(-p.x, -p.y); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfRectRot270::get_transformed(const CoordPoint& p) const +{ + return CoordPoint(p.y, -p.x); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfRectRefl::get_transformed(const CoordPoint& p) const +{ + return CoordPoint(-p.x, p.y); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfRectRot90Refl::get_transformed(const CoordPoint& p) const +{ + return CoordPoint(-p.y, -p.x); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfRectRot180Refl::get_transformed(const CoordPoint& p) const +{ + return CoordPoint(p.x, -p.y); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfRectRot270Refl::get_transformed(const CoordPoint& p) const +{ + return CoordPoint(p.y, p.x); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base diff --git a/src/libboardgame_base/RectTransform.h b/src/libboardgame_base/RectTransform.h new file mode 100644 index 0000000..823de62 --- /dev/null +++ b/src/libboardgame_base/RectTransform.h @@ -0,0 +1,106 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/RectTransform.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_BASE_RECTTRANSFORM_H +#define LIBBOARDGAME_BASE_RECTTRANSFORM_H + +#include "Transform.h" + +namespace libboardgame_base { + +//----------------------------------------------------------------------------- + +class TransfIdentity + : public Transform +{ +public: + TransfIdentity() : Transform(0) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfRectRot90 + : public Transform +{ +public: + TransfRectRot90() : Transform(0) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfRectRot180 + : public Transform +{ +public: + TransfRectRot180() : Transform(0) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfRectRot270 + : public Transform +{ +public: + TransfRectRot270() : Transform(0) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfRectRefl + : public Transform +{ +public: + TransfRectRefl() : Transform(0) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfRectRot90Refl + : public Transform +{ +public: + TransfRectRot90Refl() : Transform(0) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfRectRot180Refl + : public Transform +{ +public: + TransfRectRot180Refl() : Transform(0) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfRectRot270Refl + : public Transform +{ +public: + TransfRectRot270Refl() : Transform(0) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_TRANSFORM_H diff --git a/src/libboardgame_base/StringRep.cpp b/src/libboardgame_base/StringRep.cpp new file mode 100644 index 0000000..4063939 --- /dev/null +++ b/src/libboardgame_base/StringRep.cpp @@ -0,0 +1,86 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/StringRep.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "StringRep.h" + +#include +#include +#include "libboardgame_util/StringUtil.h" +#include "libboardgame_util/Unused.h" + +namespace libboardgame_base { + +using libboardgame_util::get_letter_coord; + +//----------------------------------------------------------------------------- + +StringRep::~StringRep() = default; + +//----------------------------------------------------------------------------- + +StdStringRep::~StdStringRep() = default; + +bool StdStringRep::read(istream& in, unsigned width, unsigned height, + unsigned& x, unsigned& y) const +{ + int c; + while (true) + { + c = in.peek(); + if (c == EOF || ! isspace(c)) + break; + in.get(); + } + bool read_x = false; + x = 0; + while (true) + { + c = in.peek(); + if (c == EOF || ! isalpha(c)) + break; + c = tolower(in.get()); + if (c < 'a' || c > 'z') + return false; + x = 26 * x + (c - 'a' + 1); + read_x = true; + } + if (! read_x) + return false; + --x; + if (x >= width) + return false; + c = in.peek(); + if (c < '0' || c > '9') + return false; + in >> y; + if (! in || y > height + 1) + return false; + y = height - y; + c = in.peek(); + if (c == EOF) + { + in.clear(); + return true; + } + if (isspace(c)) + return true; + return false; +} + +void StdStringRep::write(ostream& out, unsigned x, unsigned y, unsigned width, + unsigned height) const +{ + LIBBOARDGAME_UNUSED(width); + out << get_letter_coord(x) << (height - y); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base diff --git a/src/libboardgame_base/StringRep.h b/src/libboardgame_base/StringRep.h new file mode 100644 index 0000000..db6a674 --- /dev/null +++ b/src/libboardgame_base/StringRep.h @@ -0,0 +1,55 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/StringRep.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_BASE_STRING_REP_H +#define LIBBOARDGAME_BASE_STRING_REP_H + +#include + +namespace libboardgame_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** String representation of points. */ +struct StringRep +{ + virtual ~StringRep(); + + virtual bool read(istream& in, unsigned width, unsigned height, + unsigned& x, unsigned& y) const = 0; + + virtual void write(ostream& out, unsigned x, unsigned y, unsigned width, + unsigned height) const = 0; +}; + +//----------------------------------------------------------------------------- + +/** Spreadsheet-style string representation of points. + Can be used as a template argument for libboardgame_base::Point. + Columns are represented as letters including the letter 'J'. After 'Z', + multi-letter combinations are used: 'AA', 'AB', etc. Rows are represented + by numbers starting with '1'. Note that unlike in spreadsheets, row number + 1 is at the bottom and increases to the top to be compatible with the + convention used in chess. */ +struct StdStringRep + : public StringRep +{ + ~StdStringRep(); + + bool read(istream& in, unsigned width, unsigned height, unsigned& x, + unsigned& y) const override; + + void write(ostream& out, unsigned x, unsigned y, unsigned width, + unsigned height) const override; +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_STRING_REP_H diff --git a/src/libboardgame_base/Transform.cpp b/src/libboardgame_base/Transform.cpp new file mode 100644 index 0000000..b07cf76 --- /dev/null +++ b/src/libboardgame_base/Transform.cpp @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/Transform.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Transform.h" + +namespace libboardgame_base { + +//----------------------------------------------------------------------------- + +Transform::~Transform() +{ +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base diff --git a/src/libboardgame_base/Transform.h b/src/libboardgame_base/Transform.h new file mode 100644 index 0000000..8e170dc --- /dev/null +++ b/src/libboardgame_base/Transform.h @@ -0,0 +1,55 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/Transform.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_BASE_TRANSFORM_H +#define LIBBOARDGAME_BASE_TRANSFORM_H + +#include "CoordPoint.h" + +namespace libboardgame_base { + +//----------------------------------------------------------------------------- + +/** Rotation and/or reflection of local coordinates on the board. */ +class Transform +{ +public: + virtual ~Transform(); + + virtual CoordPoint get_transformed(const CoordPoint& p) const = 0; + + /** Get the new point type of the (0,0) coordinates. + The transformation may change the point type of the (0,0) coordinates. + For example, in the Blokus Trigon board, a reflection at the y axis + changes the type from 0 (=downside triangle) to 1 (=upside triangle). + @see Geometry::get_point_type() */ + unsigned get_new_point_type() const { return m_new_point_type; } + + /** @tparam I An iterator of a container with elements of type CoordPoint */ + template + void transform(I begin, I end) const; + +protected: + explicit Transform(unsigned new_point_type) + : m_new_point_type(new_point_type) + {} + +private: + unsigned m_new_point_type; +}; + +template +void Transform::transform(I begin, I end) const +{ + for (I i = begin; i != end; ++i) + *i = get_transformed(*i); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_TRANSFORM_H diff --git a/src/libboardgame_gtp/Arguments.cpp b/src/libboardgame_gtp/Arguments.cpp new file mode 100644 index 0000000..019a619 --- /dev/null +++ b/src/libboardgame_gtp/Arguments.cpp @@ -0,0 +1,84 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_gtp/Arguments.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Arguments.h" + +#include + +namespace libboardgame_gtp { + +//----------------------------------------------------------------------------- + +void Arguments::check_size(unsigned n) const +{ + if (get_size() == n) + return; + if (n == 0) + throw Failure("no arguments allowed"); + else if (n == 1) + throw Failure("command needs one argument"); + else + { + ostringstream msg; + msg << "command needs " << n << " arguments"; + throw Failure(msg.str()); + } +} + +void Arguments::check_size_less_equal(unsigned n) const +{ + if (get_size() <= n) + return; + if (n == 1) + throw Failure("command needs at most one argument"); + else + { + ostringstream msg; + msg << "command needs at most " << n << " arguments"; + throw Failure(msg.str()); + } +} + +CmdLineRange Arguments::get(unsigned i) const +{ + if (i < get_size()) + return m_line.get_element(m_line.get_idx_name() + i + 1); + ostringstream msg; + msg << "missing argument " << (i + 1); + throw Failure(msg.str()); +} + +string Arguments::get_tolower(unsigned i) const +{ + string value = get(i); + for (auto& c : value) + c = static_cast(tolower(c)); + return value; +} + +string Arguments::get_tolower() const +{ + check_size(1); + return get_tolower(0); +} + +CmdLineRange Arguments::get_remaining_line(unsigned i) const +{ + if (i < get_size()) + return m_line.get_trimmed_line_after_elem(m_line.get_idx_name() + i + + 1); + ostringstream msg; + msg << "missing argument " << (i + 1); + throw Failure(msg.str()); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_gtp diff --git a/src/libboardgame_gtp/Arguments.h b/src/libboardgame_gtp/Arguments.h new file mode 100644 index 0000000..8a4bc4a --- /dev/null +++ b/src/libboardgame_gtp/Arguments.h @@ -0,0 +1,240 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_gtp/Arguments.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_GTP_ARGUMENTS_H +#define LIBBOARDGAME_GTP_ARGUMENTS_H + +#ifdef __GNUC__ +#include +#endif +#include +#include "CmdLine.h" +#include "Failure.h" + +namespace libboardgame_gtp { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Access arguments of command line. */ +class Arguments +{ +public: + /** Constructor. + @param line The command line (@ref libboardgame_doc_storesref) */ + explicit Arguments(const CmdLine& line); + + /** Get argument. + @param i Argument index starting with 0 + @return Argument value + @throws Failure If no such argument */ + CmdLineRange get(unsigned i) const; + + /** Get single argument. + @return Argument value + @throws Failure If no such argument or command has more than one + arguments */ + CmdLineRange get() const; + + /** Get argument converted to lowercase. + @param i Argument index starting with 0 + @return Copy of argument value converted to lowercase + @throws Failure If no such argument */ + string get_tolower(unsigned i) const; + + /** Get single argument converted to lowercase. */ + string get_tolower() const; + + /** Get argument converted to a type. + The type must implement operator<<(istream) + @param i Argument index starting with 0 + @return The converted argument + @throws Failure If no such argument, or argument cannot be converted */ + template + T parse(unsigned i) const; + + /** Get single argument converted to a type. + The type must implement operator<<(istream) + @return The converted argument + @throws Failure If no such argument, or argument cannot be converted, + or command has more than one arguments */ + template + T parse() const; + + /** Get argument converted to a type and check against a minum value. + The type must implement operator<< and operator< + @param i Argument index starting with 0 + @param min Minimum allowed value + @return Argument value + @throws Failure If no such argument, argument cannot be converted + or smaller than the mimimum value */ + template + T parse_min(unsigned i, T min) const; + + /** Get argument converted to a type and check against a range. + The type must implement operator<< and operator< + @param i Argument index starting with 0 + @param min Minimum allowed value + @param max Maximum allowed value + @return Argument value + @throws Failure If no such argument, argument cannot be converted + or not in range */ + template + T parse_min_max(unsigned i, T min, T max) const; + + template + T parse_min_max(T min, T max) const; + + /** Check that command has no arguments. + @throws Failure If command has arguments + */ + void check_empty() const; + + /** Check number of arguments. + @param n Expected number of arguments + @throws Failure If command has a different number of arguments */ + void check_size(unsigned n) const; + + /** Check maximum number of arguments. + @param n Expected maximum number of arguments + @throws Failure If command has more arguments */ + void check_size_less_equal(unsigned n) const; + + /** Get argument line. + Get all arguments as a line. + No modfications to the line were made apart from trimmimg leading + and trailing white spaces. */ + CmdLineRange get_line() const; + + /** Get number of arguments. */ + unsigned get_size() const; + + /** Return remaining line after argument. + @param i Argument index starting with 0 + @return The remaining line after the given argument, unmodified apart + from leading and trailing whitespaces, which are trimmed. Quotation + marks are not handled. + @throws Failure If no such argument */ + CmdLineRange get_remaining_line(unsigned i) const; + +private: + const CmdLine& m_line; + + template + static string get_type_name(); +}; + +inline Arguments::Arguments(const CmdLine& line) + : m_line(line) +{ +} + +inline void Arguments::check_empty() const +{ + check_size(0); +} + +inline CmdLineRange Arguments::get() const +{ + check_size(1); + return get(0); +} + +inline CmdLineRange Arguments::get_line() const +{ + return m_line.get_trimmed_line_after_elem(m_line.get_idx_name()); +} + +inline unsigned Arguments::get_size() const +{ + return + static_cast(m_line.get_elements().size()) + - m_line.get_idx_name() - 1; +} + +template +string Arguments::get_type_name() +{ +#ifdef __GNUC__ + int status; + auto name_ptr = + abi::__cxa_demangle(typeid(T).name(), nullptr, nullptr, &status); + if (status == 0) + { + string result(name_ptr); + free(name_ptr); + return result; + } + else + return typeid(T).name(); +#else + return typeid(T).name(); +#endif +} + +template +T Arguments::parse() const +{ + check_size(1); + return parse(0); +} + +template +T Arguments::parse(unsigned i) const +{ + string s = get(i); + istringstream in(s); + T result; + in >> result; + if (! in) + { + ostringstream msg; + msg << "argument " << (i + 1) << " ('" << s + << "') has invalid type (expected " << get_type_name() << ")"; + throw Failure(msg.str()); + } + return result; +} + +template +T Arguments::parse_min(unsigned i, T min) const +{ + T result = parse(i); + if (result < min) + { + ostringstream msg; + msg << "argument " << (i + 1) << " must be greater or equal " << min; + throw Failure(msg.str()); + } + return result; +} + +template +T Arguments::parse_min_max(T min, T max) const +{ + check_size(1); + return parse_min_max(0, min, max); +} + +template +T Arguments::parse_min_max(unsigned i, T min, T max) const +{ + T result = parse_min(i, min); + if (max < result) + { + ostringstream msg; + msg << "argument " << (i + 1) << " must be less or equal " << max; + throw Failure(msg.str()); + } + return result; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_gtp + +#endif // LIBBOARDGAME_GTP_ARGUMENTS_H diff --git a/src/libboardgame_gtp/CMakeLists.txt b/src/libboardgame_gtp/CMakeLists.txt new file mode 100644 index 0000000..fea26bd --- /dev/null +++ b/src/libboardgame_gtp/CMakeLists.txt @@ -0,0 +1,12 @@ +add_library(boardgame_gtp STATIC + Arguments.h + Arguments.cpp + CmdLine.h + CmdLine.cpp + CmdLineRange.h + Engine.h + Engine.cpp + Failure.h + Response.h + Response.cpp +) diff --git a/src/libboardgame_gtp/CmdLine.cpp b/src/libboardgame_gtp/CmdLine.cpp new file mode 100644 index 0000000..7fe7f5f --- /dev/null +++ b/src/libboardgame_gtp/CmdLine.cpp @@ -0,0 +1,116 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_gtp/CmdLine.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "CmdLine.h" + +#include +#include +#include + +namespace libboardgame_gtp { + +//----------------------------------------------------------------------------- + +CmdLine::~CmdLine() = default; + +void CmdLine::add_elem(string::const_iterator begin, + string::const_iterator end) +{ + // Ignore command line elements greater UINT_MAX because we use unsigned + // for element indices. + if (m_elem.size() < numeric_limits::max()) + m_elem.emplace_back(begin, end); +} + +/** Find elements (ID, command name, arguments). + Arguments are words separated by whitespaces. + Arguments with whitespaces can be quoted with quotation marks ('"'). + Characters can be escaped with a backslash ('\'). */ +void CmdLine::find_elem() +{ + m_elem.clear(); + bool escape = false; + bool is_in_string = false; + string::const_iterator begin = m_line.begin(); + string::const_iterator i; + for (i = begin; i < m_line.end(); ++i) + { + char c = *i; + if (c == '"' && ! escape) + { + if (is_in_string) + add_elem(begin, i); + begin = i + 1; + is_in_string = ! is_in_string; + } + else if (isspace(static_cast(c)) && ! is_in_string) + { + if (i > begin) + m_elem.emplace_back(begin, i); + begin = i + 1; + } + escape = (c == '\\' && ! escape); + } + if (i > begin) + m_elem.emplace_back(begin, m_line.end()); +} + +CmdLineRange CmdLine::get_trimmed_line_after_elem(unsigned i) const +{ + assert(i < m_elem.size()); + auto& e = m_elem[i]; + auto begin = e.end(); + if (begin < m_line.end() && *begin == '"') + ++begin; + while (begin < m_line.end() && isspace(static_cast(*begin))) + ++begin; + auto end = m_line.end(); + while (end > begin && isspace(static_cast(*(end - 1)))) + --end; + return CmdLineRange(begin, end); +} + +void CmdLine::init(const string& line) +{ + m_line = line; + find_elem(); + assert(! m_elem.empty()); + parse_id(); + assert(! m_elem.empty()); +} + +void CmdLine::init(const CmdLine& c) +{ + m_idx_name = c.m_idx_name; + m_line = c.m_line; + m_elem.clear(); + for (auto& i : c.m_elem) + { + auto begin = m_line.begin() + (i.begin() - c.m_line.begin()); + auto end = m_line.begin() + (i.end() - c.m_line.begin()); + m_elem.emplace_back(begin, end); + } +} + +void CmdLine::parse_id() +{ + m_idx_name = 0; + if (m_elem.size() < 2) + return; + istringstream in(m_elem[0]); + int id; + in >> id; + if (in) + m_idx_name = 1; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_gtp diff --git a/src/libboardgame_gtp/CmdLine.h b/src/libboardgame_gtp/CmdLine.h new file mode 100644 index 0000000..c51b966 --- /dev/null +++ b/src/libboardgame_gtp/CmdLine.h @@ -0,0 +1,118 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_gtp/CmdLine.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_GTP_CMDLINE_H +#define LIBBOARDGAME_GTP_CMDLINE_H + +#include +#include +#include +#include +#include +#include "CmdLineRange.h" + +namespace libboardgame_gtp { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Parsed GTP command line. + Only used internally by libboardgame_gtp::Engine. GTP command handlers + query arguments of the command line through the instance of class Arguments + given as a function argument by class Engine to the command handler. */ +class CmdLine +{ +public: + /** Construct empty command. + @warning An empty command cannot be used, before init() was called. + This constructor exists only to reuse instances. */ + CmdLine() = default; + + /** Construct with a command line. + @see init() */ + explicit CmdLine(const string& line); + + ~CmdLine(); + + void init(const string& line); + + void init(const CmdLine& c); + + const string& get_line() const; + + /** Get command name. */ + CmdLineRange get_name() const; + + void write_id(ostream& out) const; + + CmdLineRange get_trimmed_line_after_elem(unsigned i) const; + + const vector& get_elements() const; + + const CmdLineRange& get_element(unsigned i) const; + + int get_idx_name() const; + +private: + int m_idx_name; + + /** Full command line. */ + string m_line; + + vector m_elem; + + void add_elem(string::const_iterator begin, string::const_iterator end); + + void find_elem(); + + void parse_id(); +}; + +inline CmdLine::CmdLine(const string& line) +{ + init(line); +} + +inline const vector& CmdLine::get_elements() const +{ + return m_elem; +} + +inline const CmdLineRange& CmdLine::get_element(unsigned i) const +{ + assert(i < m_elem.size()); + return m_elem[i]; +} + +inline int CmdLine::get_idx_name() const +{ + return m_idx_name; +} + +inline const string& CmdLine::get_line() const +{ + return m_line; +} + +inline CmdLineRange CmdLine::get_name() const +{ + return m_elem[m_idx_name]; +} + +inline void CmdLine::write_id(ostream& out) const +{ + if (m_idx_name == 0) + return; + auto& e = m_elem[0]; + copy(e.begin(), e.end(), ostream_iterator(out)); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_gtp + +#endif // LIBBOARDGAME_GTP_CMDLINE_H diff --git a/src/libboardgame_gtp/CmdLineRange.h b/src/libboardgame_gtp/CmdLineRange.h new file mode 100644 index 0000000..24ef9ba --- /dev/null +++ b/src/libboardgame_gtp/CmdLineRange.h @@ -0,0 +1,86 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_gtp/CmdLineRange.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_GTP_CMDLINERANGE_H +#define LIBBOARDGAME_GTP_CMDLINERANGE_H + +#include +#include +#include + +namespace libboardgame_gtp { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Subrange of the GTP command line. + Avoids allocation of strings on the heap for each parsed command line. + Instances of this class are valid only during the lifetime of the command + line object. Command handlers, which access the command line through the + instance of Arguments given as a function argument, should not store + references to CmdLineRange objects. */ +struct CmdLineRange +{ + string::const_iterator m_begin; + + string::const_iterator m_end; + + + CmdLineRange(string::const_iterator begin, string::const_iterator end) + : m_begin(begin), + m_end(end) + { } + + bool operator==(const string& s) const + { + return size() == s.size() && equal(m_begin, m_end, s.begin()); + } + + bool operator!=(const string& s) const + { + return ! operator==(s); + } + + operator string() const + { + return string(m_begin, m_end); + } + + string::const_iterator begin() const + { + return m_begin; + } + + string::const_iterator end() const + { + return m_end; + } + + string::size_type size() const + { + return m_end - m_begin; + } + + void write(ostream& o) const + { + o << string(*this); + } +}; + +//----------------------------------------------------------------------------- + +inline ostream& operator<<(ostream& out, const CmdLineRange& r) +{ + r.write(out); + return out; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_gtp + +#endif // LIBBOARDGAME_GTP_CMDLINERANGE_H diff --git a/src/libboardgame_gtp/Engine.cpp b/src/libboardgame_gtp/Engine.cpp new file mode 100644 index 0000000..f0e4e04 --- /dev/null +++ b/src/libboardgame_gtp/Engine.cpp @@ -0,0 +1,243 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_gtp/Engine.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Engine.h" + +#include +#include +#include "CmdLine.h" + +namespace libboardgame_gtp { + +//----------------------------------------------------------------------------- + +/** Utility functions. */ +namespace { + +/** Check, if line contains a command. */ +bool is_cmd_line(const string& line) +{ + for (char c : line) + if (! isspace(static_cast(c))) + return c != '#'; + return false; +} + +/** Read next command from stream. + @param in The input stream. + @param[out] c The command (reused for efficiency) + @return @c false on end-of-stream or read error. */ +bool read_cmd(CmdLine& c, istream& in) +{ + string line; + while (getline(in, line)) + if (is_cmd_line(line)) + break; + if (! in.fail()) + { + c.init(line); + return true; + } + else + return false; +} + +} // namespace + +//----------------------------------------------------------------------------- + +Engine::Engine() +{ + add("known_command", &Engine::cmd_known_command); + add("list_commands", &Engine::cmd_list_commands); + add("name", &Engine::cmd_name); + add("protocol_version", &Engine::cmd_protocol_version); + add("quit", &Engine::cmd_quit); + add("version", &Engine::cmd_version); +} + +Engine::~Engine() = default; + +void Engine::add(const string& name, Handler f) +{ + m_handlers[name] = f; +} + +void Engine::add(const string& name, HandlerNoArgs f) +{ + add(name, + Handler(bind(no_args_wrapper, f, placeholders::_1, placeholders::_2))); +} + +void Engine::add(const string& name, HandlerNoResponse f) +{ + add(name, Handler(bind(no_response_wrapper, f, + placeholders::_1, placeholders::_2))); +} + +void Engine::add(const string& name, HandlerNoArgsNoResponse f) +{ + add(name, Handler(bind(no_args_no_response_wrapper, f, + placeholders::_1, placeholders::_2))); +} + +/** Return @c true if command is known, @c false otherwise. */ +void Engine::cmd_known_command(const Arguments& args, Response& response) +{ + response.set(contains(args.get()) ? "true" : "false"); +} + +/** List all known commands. */ +void Engine::cmd_list_commands(Response& response) +{ + for (auto& i : m_handlers) + response << i.first << '\n'; +} + +/** Return name. */ +void Engine::cmd_name(Response& response) +{ + response.set("Unknown"); +} + +/** Return protocol version. */ +void Engine::cmd_protocol_version(Response& response) +{ + response.set("2"); +} + +/** Quit command loop. */ +void Engine::cmd_quit() +{ + m_quit = true; +} + +/** Return empty version string. + The GTP standard says to return empty string, if no meaningful response + is available. */ +void Engine::cmd_version(Response&) +{ +} + +bool Engine::contains(const string& name) const +{ + return m_handlers.count(name) > 0; +} + +bool Engine::exec(istream& in, bool throw_on_fail, ostream* log) +{ + string line; + Response response; + string buffer; + CmdLine cmd; + while (getline(in, line)) + { + if (! is_cmd_line(line)) + continue; + cmd.init(line); + if (log) + *log << cmd.get_line() << '\n'; + bool status = handle_cmd(cmd, log, response, buffer); + if (! status && throw_on_fail) + { + ostringstream msg; + msg << "executing '" << cmd.get_line() << "' failed"; + throw Failure(msg.str()); + } + } + return ! in.fail(); +} + +void Engine::exec_main_loop(istream& in, ostream& out) +{ + m_quit = false; + CmdLine cmd; + Response response; + string buffer; + while (! m_quit) + { + if (read_cmd(cmd, in)) + handle_cmd(cmd, &out, response, buffer); + else + break; + } +} + +/** Call the handler of a command and write its response. + @param line The command + @param out The output stream for the response + @param response A reusable response instance to avoid memory allocation in + each function call + @param buffer A reusable string instance to avoid memory allocation in each + function call */ +bool Engine::handle_cmd(CmdLine& line, ostream* out, Response& response, + string& buffer) +{ + on_handle_cmd_begin(); + bool status = true; + try + { + response.clear(); + auto pos = m_handlers.find(line.get_name()); + if (pos != m_handlers.end()) + { + Arguments args(line); + (pos->second)(args, response); + } + else + { + status = false; + response << "unknown command (" << line.get_name() << ')'; + } + } + catch (const Failure& failure) + { + status = false; + response.set(failure.what()); + } + if (out) + { + *out << (status ? '=' : '?'); + line.write_id(*out); + *out << ' '; + response.write(*out, buffer); + out->flush(); + } + return status; +} + +void Engine::no_args_wrapper(HandlerNoArgs h, const Arguments& args, + Response& response) +{ + args.check_empty(); + h(response); +} + +void Engine::no_response_wrapper(HandlerNoResponse h, const Arguments& args, + Response&) +{ + h(args); +} + +void Engine::no_args_no_response_wrapper(HandlerNoArgsNoResponse h, + const Arguments& args, Response&) +{ + args.check_empty(); + h(); +} + +void Engine::on_handle_cmd_begin() +{ + // Default implementation does nothing +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_gtp diff --git a/src/libboardgame_gtp/Engine.h b/src/libboardgame_gtp/Engine.h new file mode 100644 index 0000000..b043164 --- /dev/null +++ b/src/libboardgame_gtp/Engine.h @@ -0,0 +1,227 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_gtp/Engine.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_GTP_ENGINE_H +#define LIBBOARDGAME_GTP_ENGINE_H + +#include +#include +#include +#include "Arguments.h" +#include "Response.h" + +namespace libboardgame_gtp { + +class CmdLine; + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Base class for GTP engines. + Commands can be added with Engine::add(). Existing commands can be + overridden by registering a new handler for the command. + @see @ref libboardgame_gtp_commands */ +class Engine +{ +public: + typedef function Handler; + + typedef function HandlerNoArgs; + + typedef function HandlerNoResponse; + + typedef function HandlerNoArgsNoResponse; + + /** @page libboardgame_gtp_commands libboardgame_gtp::Engine GTP commands +

+
@link cmd_known_command() @c known_command @endlink
+
@copydoc cmd_known_command()
+
@link cmd_list_commands() @c list_commands @endlink
+
@copydoc cmd_list_commands()
+
@link cmd_name() @c name @endlink
+
@copydoc cmd_name()
+
@link cmd_protocol_version() @c protocol_version @endlink
+
@copydoc cmd_protocol_version()
+
@link cmd_quit() @c quit @endlink
+
@copydoc cmd_quit()
+
@link cmd_version() @c version @endlink
+
@copydoc cmd_version()
+
*/ + /** @name Command handlers */ + /** @{ */ + void cmd_known_command(const Arguments&, Response&); + void cmd_list_commands(Response&); + void cmd_name(Response&); + void cmd_protocol_version(Response&); + void cmd_quit(); + void cmd_version(Response&); + /** @} */ // @name + + Engine(); + + Engine(const Engine&) = delete; + + Engine& operator=(const Engine&) const = delete; + + virtual ~Engine(); + + /** Execute commands from an input stream. + @param in The input stream + @param throw_on_fail Whether to throw an exception if a command fails, + or to continue executing the remainign commands + @param log Stream for logging the commands and responses to. + @return The stream state as a bool + @throws Failure If a command fails, and @c throw_on_fail is @c true */ + bool exec(istream& in, bool throw_on_fail, ostream* log); + + /** Run the main command loop. + Reads lines from input stream, calls the corresponding command handler + and writes the response to the output stream. Empty lines in the + command responses will be replaced by a line containing a single space, + because empty lines are not allowed in GTP responses. */ + void exec_main_loop(istream& in, ostream& out); + + /** Register command handler. + If a command was already registered with the same name, it will be + replaced by the new command. */ + void add(const string& name, Handler f); + + void add(const string& name, HandlerNoArgs f); + + void add(const string& name, HandlerNoResponse f); + + void add(const string& name, HandlerNoArgsNoResponse f); + + /** Register a member function as a command handler. + If a command was already registered with the same name, it will be + replaced by the new command. */ + template + void add(const string& name, + void (T::*f)(const Arguments&, Response&), T* t); + + template + void add(const string& name, void (T::*f)(const Arguments&), T* t); + + template + void add(const string& name, void (T::*f)(Response&), T* t); + + template + void add(const string& name, void (T::*f)(), T* t); + + /** Returns if command registered. */ + bool contains(const string& name) const; + +protected: + /** Hook function to be executed before each command. + The default implementation does nothing. */ + virtual void on_handle_cmd_begin(); + + /** Register a member function of the current instance as a command + handler. + If a command was already registered with the same name, it will be + replaced by the new command. */ + template + void add(const string& name, void (T::*f)(const Arguments&, Response&)); + + template + void add(const string& name, void (T::*f)(const Arguments&)); + + template + void add(const string& name, void (T::*f)(Response&)); + + template + void add(const string& name, void (T::*f)()); + +private: + /** Mapping of command name to command handler. + They key is a string subrange, not a string, to allow looking up the + command name using Command::name_as_subrange() without creating a + temporary string for the command name. The value of type CmdInfo with + the name string and callback function are stored in an object allocated + on the heap to ensure that the range stays valid, if the value object + is copied. */ + typedef map Handlers; + + + /** Flag to quit main loop. */ + bool m_quit; + + Handlers m_handlers; + + + bool handle_cmd(CmdLine& line, ostream* out, Response& response, + string& buffer); + + static void no_args_wrapper(HandlerNoArgs h, + const Arguments& args, Response& response); + + static void no_response_wrapper(HandlerNoResponse h, + const Arguments& args, Response&); + + static void no_args_no_response_wrapper(HandlerNoArgsNoResponse h, + const Arguments& args, Response&); +}; + +template +void Engine::add(const string& name, void (T::*f)(const Arguments&, Response&)) +{ + add(name, f, dynamic_cast(this)); +} + +template +void Engine::add(const string& name, void (T::*f)(Response&)) +{ + add(name, f, dynamic_cast(this)); +} + +template +void Engine::add(const string& name, void (T::*f)(const Arguments&)) +{ + add(name, f, dynamic_cast(this)); +} + +template +void Engine::add(const string& name, void (T::*f)()) +{ + add(name, f, dynamic_cast(this)); +} + +template +void Engine::add(const string& name, + void (T::*f)(const Arguments&, Response&), T* t) +{ + assert(f); + add(name, + static_cast(bind(f, t, placeholders::_1, placeholders::_2))); +} + +template +void Engine::add(const string& name, void (T::*f)(Response&), T* t) +{ + assert(f); + add(name, static_cast(bind(f, t, placeholders::_1))); +} + +template +void Engine::add(const string& name, void (T::*f)(const Arguments&), T* t) +{ + assert(f); + add(name, static_cast(bind(f, t, placeholders::_1))); +} + +template +void Engine::add(const string& name, void (T::*f)(), T* t) +{ + assert(f); + add(name, static_cast(bind(f, t))); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_gtp + +#endif // LIBBOARDGAME_GTP_ENGINE_H diff --git a/src/libboardgame_gtp/Failure.h b/src/libboardgame_gtp/Failure.h new file mode 100644 index 0000000..90eee8a --- /dev/null +++ b/src/libboardgame_gtp/Failure.h @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_gtp/Failure.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_GTP_FAILURE_H +#define LIBBOARDGAME_GTP_FAILURE_H + +#include + +namespace libboardgame_gtp { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** GTP failure. + Command handlers generate a GTP error response by throwing an instance + of Failure. */ +class Failure + : public runtime_error +{ + using runtime_error::runtime_error; +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_gtp + +#endif // LIBBOARDGAME_GTP_ENGINE_H diff --git a/src/libboardgame_gtp/Response.cpp b/src/libboardgame_gtp/Response.cpp new file mode 100644 index 0000000..5868b3e --- /dev/null +++ b/src/libboardgame_gtp/Response.cpp @@ -0,0 +1,40 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_gtp/Response.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Response.h" + +namespace libboardgame_gtp { + +//----------------------------------------------------------------------------- + +ostringstream Response::s_dummy; + +Response::~Response() = default; + +void Response::write(ostream& out, string& buffer) const +{ + buffer = m_stream.str(); + bool was_newline = false; + for (auto c : buffer) + { + bool is_newline = (c == '\n'); + if (is_newline && was_newline) + out << ' '; + out << c; + was_newline = is_newline; + } + if (! was_newline) + out << '\n'; + out << '\n'; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_gtp diff --git a/src/libboardgame_gtp/Response.h b/src/libboardgame_gtp/Response.h new file mode 100644 index 0000000..2805818 --- /dev/null +++ b/src/libboardgame_gtp/Response.h @@ -0,0 +1,86 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_gtp/Response.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_GTP_RESPONSE_H +#define LIBBOARDGAME_GTP_RESPONSE_H + +#include +#include + +namespace libboardgame_gtp { + +using namespace std; + +//----------------------------------------------------------------------------- + +class Response +{ +public: + ~Response(); + + /** Conversion to output stream. + Returns reference to response stream. */ + operator ostream&(); + + /** Get response. + @return A copy of the internal response string stream */ + string to_string() const; + + /** Set response. */ + void set(const string& response); + + void clear(); + + /** Write response to output stream. + Also sanitizes responses containing empty lines ("\n\n" cannot occur + in a response, because it means end of response; it will be replaced by + "\n \n") and adds "\n\n" add the end of the response. */ + void write(ostream& out, string& buffer) const; + +private: + /** Dummy stream for copying default formatting settings. */ + static ostringstream s_dummy; + + /** Response stream */ + ostringstream m_stream; +}; + +inline Response::operator ostream&() +{ + return m_stream; +} + +inline void Response::clear() +{ + m_stream.str(""); + m_stream.copyfmt(s_dummy); +} + +inline string Response::to_string() const +{ + return m_stream.str(); +} + +inline void Response::set(const string& response) +{ + m_stream.str(response); +} + +//----------------------------------------------------------------------------- + +/** @relates libboardgame_gtp::Response */ +template +inline Response& operator<<(Response& r, const TYPE& t) +{ + static_cast(r) << t; + return r; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_gtp + +#endif // LIBBOARDGAME_GTP_RESPONSE_H diff --git a/src/libboardgame_mcts/Atomic.h b/src/libboardgame_mcts/Atomic.h new file mode 100644 index 0000000..bf6bdb8 --- /dev/null +++ b/src/libboardgame_mcts/Atomic.h @@ -0,0 +1,101 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_mcts/Atomic.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_MCTS_ATOMIC_H +#define LIBBOARDGAME_MCTS_ATOMIC_H + +#include +#include "libboardgame_util/Unused.h" + +namespace libboardgame_mcts { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Data that may be atomic. + This struct is used for sharing the same code for a single-threaded and + a multi-threaded implementation depending on a template argument. + In the multi-threaded implementation, the variable is atomic, which + usually causes a small performance penalty, in the single-threaded + implementation, it is simply a regular variable. + @param T The type of the variable. + @param MT true, if the variable should be atomic. */ +template struct Atomic; + +template +struct Atomic +{ + T val; + + T operator=(T t) + { + val = t; + return val; + } + + T load(memory_order order = memory_order_seq_cst) const + { + LIBBOARDGAME_UNUSED(order); + return val; + } + + void store(T t, memory_order order = memory_order_seq_cst) + { + LIBBOARDGAME_UNUSED(order); + val = t; + } + + operator T() const + { + return val; + } + + T fetch_add(T t) + { + T tmp = val; + val += t; + return tmp; + } +}; + +template +struct Atomic +{ + atomic val; + + T operator=(T t) + { + val.store(t); + return val; + } + + T load(memory_order order = memory_order_seq_cst) const + { + return val.load(order); + } + + void store(T t, memory_order order = memory_order_seq_cst) + { + val.store(t, order); + } + + operator T() const + { + return load(); + } + + T fetch_add(T t) + { + return val.fetch_add(t); + } +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_mcts + +#endif // LIBBOARDGAME_MCTS_ATOMIC_H diff --git a/src/libboardgame_mcts/CMakeLists.txt b/src/libboardgame_mcts/CMakeLists.txt new file mode 100644 index 0000000..e0b4461 --- /dev/null +++ b/src/libboardgame_mcts/CMakeLists.txt @@ -0,0 +1,11 @@ +# This library contains only header files with templates. The empty target +# exists only to add the headers to IDE project files. +add_custom_target(boardgame_mcts SOURCES + Atomic.h + LastGoodReply.h + Node.h + PlayerMove.h + SearchBase.h + Tree.h + TreeUtil.h +) diff --git a/src/libboardgame_mcts/LastGoodReply.h b/src/libboardgame_mcts/LastGoodReply.h new file mode 100644 index 0000000..25e4485 --- /dev/null +++ b/src/libboardgame_mcts/LastGoodReply.h @@ -0,0 +1,154 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_mcts/LastGoodReply.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_MCTS_LAST_GOOD_REPLY_H +#define LIBBOARDGAME_MCTS_LAST_GOOD_REPLY_H + +#include +#include +#include +#include +#include "Atomic.h" +#include "PlayerMove.h" + +namespace libboardgame_mcts { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Storage for Last-Good-Reply heuristic. + Uses LGRF-2 (Baier, Drake: The Power of Forgetting: Improving the + Last-Good-Reply Policy in Monte-Carlo Go. 2010. + http://webdisk.lclark.edu/drake/publications/baier-drake-ieee-2010.pdf) + To save space, only the player of the reply move is considered when storing + or receiving a reply, the players of the last and second last moves are + ignored. In games without a fixed order of players (i.e. when move + sequences with the same moves but not played by the same players occur), + this can cause undetected collisions. If these collisions are not + sufficiently rare, the last-good-reply heuristic should be disabled in the + search. Undetected collisions can also occur because the replies are stored + in a hash table without collision check. But since the replies have to be + checked for legality in the current position anyway and the collisions are + probably rare, no major negative effect is expected from these collisions. + @tparam M The move type. + @tparam P The (maximum) number of players. + @tparam S The number of entries in the LGR2 has table (per player). + @tparam MT Whether the LGR table is used in a multi-threaded search. */ +template +class LastGoodReply +{ +public: + typedef M Move; + + static const unsigned max_players = P; + + static const size_t hash_table_size = S; + + LastGoodReply(); + + void init(PlayerInt nu_players); + + void store(PlayerInt player, Move last, Move second_last, Move reply); + + void forget(PlayerInt player, Move last, Move second_last, Move reply); + + Move get_lgr1(PlayerInt player, Move last) const; + + Move get_lgr2(PlayerInt player, Move last, Move second_last) const; + +private: + size_t m_hash[Move::range]; + + Atomic m_lgr1[max_players][Move::range]; + + Atomic m_lgr2[max_players][hash_table_size]; + + size_t get_index(Move last, Move second_last) const; +}; + +template +LastGoodReply::LastGoodReply() +{ + mt19937 generator; + for (auto& hash : m_hash) + hash = generator(); +} + +template +inline size_t LastGoodReply::get_index(Move last, + Move second_last) const +{ + size_t hash = (m_hash[last.to_int()] ^ m_hash[second_last.to_int()]); + return hash % hash_table_size; +} + +template +inline auto LastGoodReply::get_lgr1(PlayerInt player, + Move last) const -> Move +{ + return Move(m_lgr1[player][last.to_int()].load(memory_order_relaxed)); +} + +template +inline auto LastGoodReply::get_lgr2( + PlayerInt player, Move last, Move second_last) const -> Move +{ + auto index = get_index(last, second_last); + return Move(m_lgr2[player][index].load(memory_order_relaxed)); +} + +template +void LastGoodReply::init(PlayerInt nu_players) +{ + for (PlayerInt i = 0; i < nu_players; ++i) + if (Move::null().to_int() == 0) + { + // Using memset is ok even if the elements are atomic because + // init() is used before the multi-threaded search starts. + memset(m_lgr1[i], 0, Move::range * sizeof(m_lgr1[i][0])); + memset(m_lgr2[i], 0, hash_table_size * sizeof(m_lgr2[i][0])); + } + else + { + fill(m_lgr1[i], m_lgr1[i] + Move::range, Move::null().to_int()); + fill(m_lgr2[i], m_lgr2[i] + hash_table_size, + Move::null().to_int()); + } +} + +template +inline void LastGoodReply::forget(PlayerInt player, Move last, + Move second_last, Move reply) +{ + auto reply_int = reply.to_int(); + auto null_int = Move::null().to_int(); + { + auto index = get_index(last, second_last); + auto& stored_reply = m_lgr2[player][index]; + if (stored_reply.load(memory_order_relaxed) == reply_int) + stored_reply.store(null_int, memory_order_relaxed); + } + auto& stored_reply = m_lgr1[player][last.to_int()]; + if (stored_reply.load(memory_order_relaxed) == reply_int) + stored_reply.store(null_int, memory_order_relaxed); +} + +template +inline void LastGoodReply::store(PlayerInt player, Move last, + Move second_last, Move reply) +{ + auto reply_int = reply.to_int(); + auto index = get_index(last, second_last); + m_lgr2[player][index].store(reply_int, memory_order_relaxed); + m_lgr1[player][last.to_int()].store(reply_int, memory_order_relaxed); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_mcts + +#endif // LIBBOARDGAME_MCTS_LAST_GOOD_REPLY_H diff --git a/src/libboardgame_mcts/Node.h b/src/libboardgame_mcts/Node.h new file mode 100644 index 0000000..5bb8bdd --- /dev/null +++ b/src/libboardgame_mcts/Node.h @@ -0,0 +1,292 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_mcts/Node.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_MCTS_NODE_H +#define LIBBOARDGAME_MCTS_NODE_H + +#include +#include "Atomic.h" +#include "libboardgame_util/Assert.h" + +namespace libboardgame_mcts { + +using namespace std; + +//----------------------------------------------------------------------------- + +typedef uint_least32_t NodeIdx; + +//----------------------------------------------------------------------------- + +/** %Node in a MCTS tree. + For details about how the nodes are used in lock-free multi-threaded mode, + see @ref libboardgame_doc_enz_2009. */ +template +class Node +{ +public: + typedef M Move; + + typedef F Float; + + Node() = default; + + Node(const Node&) = delete; + + Node& operator=(const Node&) = delete; + + /** Initialize the node. + This function may not be called on a node that is already part of + the tree in multi-threaded mode. + The node may be initialized with values and counts greater zero + (prior knowledge) but even if it is initialized with count zero, it + must be initialized with a usable value (e.g. first play urgency for + inner nodes or tie value for the root node). */ + void init(const Move& mv, Float value, Float count); + + /** Initializes the root node. + Does not initialize value and value count as they are not used for the + root. */ + void init_root(); + + const Move& get_move() const; + + /** Number of simulations that went through this node. */ + Float get_visit_count() const; + + /** Number of values that were added. + This count is usually larger than the visit count because in addition + to the terminal values of the simulations, prior knowledge values and + weighted RAVE values could have been added added. */ + Float get_value_count() const; + + /** Value of the node. + For the root node, this is the value of the position from the point of + view of the player at the root node; for all other nodes, this is the + value of the move leading to the position at the node from the point + of view of the player at the parent node. */ + Float get_value() const; + + bool has_children() const; + + unsigned short get_nu_children() const; + + /** Copy the value count from another node without changing the child + information. + This function is not thread-safe and may not be called during the + search. */ + void copy_data_from(const Node& node); + + void link_children(NodeIdx first_child, unsigned short nu_children); + + /** Faster version of link_children() for single-threaded parts of the + code. */ + void link_children_st(NodeIdx first_child, unsigned short nu_children); + + void unlink_children(); + + /** Faster version of unlink_children() for single-threaded parts of the + code. */ + void unlink_children_st(); + + void add_value(Float v, Float weight = 1); + + /** Add a value with weight 1 and remove a previously added loss. + Needed for the implementation of virtual losses in multi-threaded + MCTS and more efficient that a separate add and remove call. */ + void add_value_remove_loss(Float v); + + void inc_visit_count(); + + /** Get node index of first child. + @pre has_children() */ + NodeIdx get_first_child() const; + +private: + Atomic m_value; + + Atomic m_value_count; + + Atomic m_visit_count; + + Atomic m_nu_children; + + Move m_move; + + NodeIdx m_first_child; +}; + +template +void Node::add_value(Float v, Float weight) +{ + // Intentionally uses no synchronization and does not care about + // lost updates in multi-threaded mode + Float count = m_value_count.load(memory_order_relaxed); + Float value = m_value.load(memory_order_relaxed); + count += weight; + value += weight * (v - value) / count; + m_value.store(value, memory_order_relaxed); + m_value_count.store(count, memory_order_relaxed); +} + +template +void Node::add_value_remove_loss(Float v) +{ + // Intentionally uses no synchronization and does not care about + // lost updates in multi-threaded mode + Float count = m_value_count.load(memory_order_relaxed); + if (count == 0) + return; // Adding the virtual loss was a lost update + Float value = m_value.load(memory_order_relaxed); + value += v / count; + m_value.store(value, memory_order_relaxed); +} + +template +void Node::copy_data_from(const Node& node) +{ + // Reminder to update this function when the class gets additional members + struct Dummy + { + Atomic m_value; + Atomic m_value_count; + Atomic m_visit_count; + Atomic m_nu_children; + Move m_move; + NodeIdx m_first_child; + }; + static_assert(sizeof(Node) == sizeof(Dummy), ""); + + m_move = node.m_move; + // Load/store relaxed (it wouldn't even need to be atomic) because this + // function is only used before the multi-threaded search. + m_value_count.store(node.m_value_count.load(memory_order_relaxed), + memory_order_relaxed); + m_value.store(node.m_value.load(memory_order_relaxed), + memory_order_relaxed); + m_visit_count.store(node.m_visit_count.load(memory_order_relaxed), + memory_order_relaxed); +} + +template +inline auto Node::get_value_count() const -> Float +{ + return m_value_count.load(memory_order_relaxed); +} + +template +inline NodeIdx Node::get_first_child() const +{ + LIBBOARDGAME_ASSERT(has_children()); + return m_first_child; +} + +template +inline auto Node::get_move() const -> const Move& +{ + return m_move; +} + +template +inline unsigned short Node::get_nu_children() const +{ + return m_nu_children.load(memory_order_acquire); +} + +template +inline auto Node::get_value() const -> Float +{ + return m_value.load(memory_order_relaxed); +} + +template +inline auto Node::get_visit_count() const -> Float +{ + return m_visit_count.load(memory_order_relaxed); +} + +template +inline bool Node::has_children() const +{ + return get_nu_children() > 0; +} + +template +inline void Node::inc_visit_count() +{ + // We don't care about the unlikely case that updates are lost because + // incrementing is not atomic + Float count = m_visit_count.load(memory_order_relaxed); + ++count; + m_visit_count.store(count, memory_order_relaxed); +} + +template +void Node::init(const Move& mv, Float value, Float count) +{ + // The node is not yet visible to other threads because init() is called + // before the children are linked to its parent with link_children() + // (which does a memory_order_release on m_nu_children of the parent). + // Therefore, the most efficient way here is to initialize all values with + // memory_order_relaxed. + m_move = mv; + m_value_count.store(count, memory_order_relaxed); + m_value.store(value, memory_order_relaxed); + m_visit_count.store(0, memory_order_relaxed); + m_nu_children.store(0, memory_order_relaxed); +} + +template +void Node::init_root() +{ +#if LIBBOARDGAME_DEBUG + m_move = Move::null(); +#endif + m_visit_count.store(0, memory_order_relaxed); + m_nu_children.store(0, memory_order_relaxed); +} + +template +inline void Node::link_children(NodeIdx first_child, + unsigned short nu_children) +{ + LIBBOARDGAME_ASSERT(nu_children < Move::range); + // first_child cannot be 0 because 0 is always used for the root node + LIBBOARDGAME_ASSERT(first_child != 0); + m_first_child = first_child; + m_nu_children.store(nu_children, memory_order_release); +} + +template +inline void Node::link_children_st(NodeIdx first_child, + unsigned short nu_children) +{ + LIBBOARDGAME_ASSERT(nu_children < Move::range); + // first_child cannot be 0 because 0 is always used for the root node + LIBBOARDGAME_ASSERT(first_child != 0); + m_first_child = first_child; + // Store relaxed (wouldn't even need to be atomic) + m_nu_children.store(nu_children, memory_order_relaxed); +} + +template +inline void Node::unlink_children() +{ + m_nu_children.store(0, memory_order_release); +} + +template +inline void Node::unlink_children_st() +{ + // Store relaxed (wouldn't even need to be atomic) + m_nu_children.store(0, memory_order_relaxed); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_mcts + +#endif // LIBBOARDGAME_MCTS_NODE_H diff --git a/src/libboardgame_mcts/PlayerMove.h b/src/libboardgame_mcts/PlayerMove.h new file mode 100644 index 0000000..1bf6b9a --- /dev/null +++ b/src/libboardgame_mcts/PlayerMove.h @@ -0,0 +1,40 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_mcts/PlayerMove.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_MCTS_PLAYER_MOVE_H +#define LIBBOARDGAME_MCTS_PLAYER_MOVE_H + +#include + +namespace libboardgame_mcts { + +//----------------------------------------------------------------------------- + +typedef uint_fast8_t PlayerInt; + +//----------------------------------------------------------------------------- + +template +struct PlayerMove +{ + PlayerInt player; + + MOVE move; + + PlayerMove() = default; + + PlayerMove(PlayerInt player, MOVE move) + { + this->player = player; + this->move = move; + } +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_mcts + +#endif // LIBBOARDGAME_MCTS_PLAYER_MOVE_H diff --git a/src/libboardgame_mcts/SearchBase.h b/src/libboardgame_mcts/SearchBase.h new file mode 100644 index 0000000..1a99927 --- /dev/null +++ b/src/libboardgame_mcts/SearchBase.h @@ -0,0 +1,1544 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_mcts/SearchBase.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_MCTS_SEARCH_BASE_H +#define LIBBOARDGAME_MCTS_SEARCH_BASE_H + +#include +#include +#include +#include +#include +#include "Atomic.h" +#include "LastGoodReply.h" +#include "PlayerMove.h" +#include "Tree.h" +#include "TreeUtil.h" +#include "libboardgame_util/Abort.h" +#include "libboardgame_util/ArrayList.h" +#include "libboardgame_util/Barrier.h" +#include "libboardgame_util/IntervalChecker.h" +#include "libboardgame_util/Log.h" +#include "libboardgame_util/MathUtil.h" +#include "libboardgame_util/Statistics.h" +#include "libboardgame_util/StringUtil.h" +#include "libboardgame_util/TimeIntervalChecker.h" +#include "libboardgame_util/Timer.h" +#include "libboardgame_util/Unused.h" + +namespace libboardgame_mcts { + +using namespace std; +using libboardgame_mcts::tree_util::find_node; +using libboardgame_util::get_abort; +using libboardgame_util::time_to_string; +using libboardgame_util::to_string; +using libboardgame_util::ArrayList; +using libboardgame_util::Barrier; +using libboardgame_util::IntervalChecker; +using libboardgame_util::RandomGenerator; +using libboardgame_util::StatisticsBase; +using libboardgame_util::StatisticsDirtyLockFree; +using libboardgame_util::StatisticsExt; +using libboardgame_util::Timer; +using libboardgame_util::TimeIntervalChecker; +using libboardgame_util::TimeSource; + +//----------------------------------------------------------------------------- + +#define LIBBOARDGAME_LOG_THREAD(thread_state, ...) \ + LIBBOARDGAME_LOG('[', thread_state.thread_id, "] ", __VA_ARGS__) + +//----------------------------------------------------------------------------- + +/** Default optional compile-time parameters for Search. + See description of class Search for more information. */ +struct SearchParamConstDefault +{ + /** The floating type used for mean values and counts. + The default type is @c float for a reduced node size and performance + gains (especially on 32-bit systems). However, using @c float sets a + practical limit on the number of simulations before the count and mean + values go into saturation. This maximum is given by 2^d-1 with d being + the digits in the mantissa (=23 for IEEE 754 float's). The search will + terminate when this number is reached. For longer searches, the code + should be compiled with floating type @c double. */ + typedef float Float; + + /** The maximum number of players. */ + static const PlayerInt max_players = 2; + + /** The maximum length of a game. */ + static const unsigned max_moves = 1000; + + /** Compile with support for multi-threaded search. + Disabling this slightly increases the performance if support for a + multi-threaded search is not needed. */ + static const bool multithread = true; + + /** Use RAVE. */ + static const bool rave = false; + + /** Enable distance weighting of RAVE updates. + The weight decreases linearly from the start to the end of a + simulation. The distance weight is applied in addition to the normal + RAVE weight. */ + static const bool rave_dist_weighting = false; + + /** Enable Last-Good-Reply heuristic. + @see LastGoodReply */ + static const bool use_lgr = false; + + /** See LastGoodReply::hash_table_size. + Must be greater 0 if use_lgr is true. */ + static const size_t lgr_hash_table_size = 0; + + /** Use virtual loss in multi-threaded mode. + See Chaslot et al.: Parallel Monte-Carlo Tree Search. 2008. */ + static const bool virtual_loss = false; + + /** Terminate search early if move is unlikely to change. + See implementation of check_cannot_change(). */ + static const bool use_unlikely_change = true; + + /** The minimum count used in prior knowledge initialization of + the children of an expanded node. + The value must be greater 0 (it may be a positive epsilon) because + otherwise the search would need to handle a special case in the bias + term computation. */ + static constexpr Float child_min_count = 0; + + /** An evaluation value representing a 50% winning probability. */ + static constexpr Float tie_value = 0.5f; + + /** Value to start the tree pruning with. + This value should be above typical count initializations if prior + knowledge initialization is used. */ + static constexpr Float prune_count_start = 16; + + /** Expected simulations per second. + If the simulations per second vary a lot, it should be a value closer + to the lower values. This value is used, for example, to determine an + interval for checking expensive abort conditions in deterministic mode + (in regular mode, the simulations per second will be measured and the + interval will be adjusted automatically). That means that in + deterministic mode, a pessimistic low value will cause more calls to + the expensive function but an optimistic high value will delay aborting + the search. */ + static constexpr double expected_sim_per_sec = 100; +}; + +//----------------------------------------------------------------------------- + +/** Game-independent Monte-Carlo tree search. + Game-dependent functionality is added by implementing some pure virtual + functions and by template parameters. + + RAVE (see @ref libboardgame_doc_rave) is implemented differently from + the algorithm described in the original paper: RAVE values are not stored + separately in the nodes but added to the normal values with a certain + (constant) weight and up to a maximum visit count of the parent node. This + saves memory in the tree and speeds up move selection in the in-tree phase. + It is weaker than the original RAVE at a low number of simulations but + seems to be equally good or even better at a high number of simulations. + + @tparam S The game-dependent state of a simulation. The state provides + functions for move generation, evaluation of terminal positions, etc. The + state should be thread-safe to support multiple states if multi-threading + is used. + @tparam M The move type. The type must be convertible to an integer by + providing M::to_int() and M::range. + @tparam R Optional compile-time parameters, see SearchParamConstDefault */ +template +class SearchBase +{ +public: + typedef S State; + + typedef M Move; + + typedef R SearchParamConst; + + static const bool multithread = SearchParamConst::multithread; + + typedef typename SearchParamConst::Float Float; + + typedef libboardgame_mcts::Node Node; + + typedef libboardgame_mcts::Tree Tree; + + typedef libboardgame_mcts::PlayerMove PlayerMove; + + static const PlayerInt max_players = SearchParamConst::max_players; + + static const unsigned max_moves = SearchParamConst::max_moves; + + static const size_t lgr_hash_table_size = + SearchParamConst::lgr_hash_table_size; + + static_assert(! SearchParamConst::use_lgr || lgr_hash_table_size > 0, ""); + + + /** Constructor. + @param nu_threads + @param memory The memory to be used for (all) the search trees. */ + SearchBase(unsigned nu_threads, size_t memory); + + virtual ~SearchBase(); + + + /** @name Pure virtual functions */ + /** @{ */ + + /** Create a new game-specific state to be used in a thread of the + search. */ + virtual unique_ptr create_state() = 0; + + /** Get the current number of players. */ + virtual PlayerInt get_nu_players() const = 0; + + /** Get player to play at root node of the search. */ + virtual PlayerInt get_player() const = 0; + + /** @} */ // @name + + + /** @name Virtual functions */ + /** @{ */ + + /** Check if the position at the root is a follow-up position of the last + search. + In this function, the subclass can store the game state at the root of + the search, compare it to the the one of the last search, check if + the current state is a follow-up position and return the move sequence + leading from the last position to the current one, so that the search + can check if a subtree of the last search can be reused. + This function will be called exactly once at the beginning of each + search. The default implementation returns false. + The information is also used for deciding whether to clear other + caches from the last search (e.g. Last-Good-Reply heuristic). */ + virtual bool check_followup(ArrayList& sequence); + + virtual string get_info() const; + + virtual string get_info_ext() const; + + /** @} */ // @name + + + /** @name Parameters */ + /** @{ */ + + /** Minimum count of a node to be expanded. */ + void set_expand_threshold(Float n); + + Float get_expand_threshold() const; + + /** Increase of the expand threshold per in-tree move played. */ + void set_expand_threshold_inc(Float n); + + Float get_expand_threshold_inc() const; + + /** Constant used in the exploration term. + The exploration term has the form c * sqrt(parent_count) / child_count + with a configurable constant c. It assumes that children counts are + initialized greater than 0. */ + void set_exploration_constant(Float c) { m_exploration_constant = c; } + + Float get_exploration_constant() const { return m_exploration_constant; } + + /** Reuse the subtree from the previous search if the current position is + a follow-up position of the previous one. */ + void set_reuse_subtree(bool enable); + + bool get_reuse_subtree() const; + + /** Reuse the tree from the previous search if the current position is + the same position as the previous one. */ + void set_reuse_tree(bool enable); + + bool get_reuse_tree() const; + + /** Maximum parent visit count for applying RAVE. */ + void set_rave_parent_max(Float value); + + Float get_rave_parent_max() const; + + /** Maximum child value count for applying RAVE. */ + void set_rave_child_max(Float value); + + Float get_rave_child_max() const; + + /** Weight used for adding RAVE values to the node value. */ + void set_rave_weight(Float value); + + Float get_rave_weight() const; + + /** @} */ // @name + + + /** Run a search. + @param[out] mv + @param max_count Number of simulations to run. The search might return + earlier if the best move cannot change anymore or if the count of the + root node was initialized from an init tree + @param min_simulations + @param max_time Maximum search time. Only used if max_count is zero + @param time_source Time source for time measurement + @return @c false if no move could be generated because the position is + a terminal position. */ + bool search(Move& mv, Float max_count, size_t min_simulations, + double max_time, TimeSource& time_source); + + const Tree& get_tree() const; + +#if LIBBOARDGAME_DEBUG + string dump() const; +#endif + + /** Number of simulations in the current search in all threads. */ + size_t get_nu_simulations() const; + + /** Select the move to play. + Uses select_final(). */ + bool select_move(Move& mv) const; + + /** Select the best child of the root node after the search. + Selects child with highest number of wins; the value is used as a + tie-breaker for equal counts (important at very low number of + simulations, e.g. all children have count 1 or 0). */ + const Node* select_final() const; + + State& get_state(unsigned thread_id); + + const State& get_state(unsigned thread_id) const; + + /** Set a callback function that informs the caller about the + estimated time left. + The callback function will be called about every 0.1s. The arguments + of the callback function are: elapsed time, estimated remaining time. */ + void set_callback(function callback); + + /** Get evaluation for a player at root node. */ + const StatisticsDirtyLockFree& get_root_val(PlayerInt player) const; + + /** Get evaluation for get_player() at root node. */ + const StatisticsDirtyLockFree& get_root_val() const; + + /** The number of times the root node was visited. + This is equal to the number of simulations plus the visit count + of a subtree reused from the previous search. */ + Float get_root_visit_count() const; + + /** Create the threads used in the search. + This cannot be done in the constructor because it uses the virtual + function create_state(). This function will automatically be called + before a search if the threads have not been constructed yet, but it + is advisable to explicitely call it in the constructor of the subclass + to save some time at the first move generation where the game clock + might already be running. */ + void create_threads(); + +protected: + struct Simulation + { + ArrayList nodes; + + ArrayList moves; + + array eval; + }; + + virtual void on_start_search(bool is_followup); + + /** Time source for current search. + Only valid during a search. */ + TimeSource& get_time_source(); + +private: +#if LIBBOARDGAME_DEBUG + class AssertionHandler + : public libboardgame_util::AssertionHandler + { + public: + AssertionHandler(const SearchBase& search); + + ~AssertionHandler(); + + void run() override; + + private: + const SearchBase& m_search; + }; +#endif + + /** Thread-specific search state. */ + struct ThreadState + { + unique_ptr state; + + unsigned thread_id; + + /** Was the search in this thread terminated because the search tree + was full? */ + bool is_out_of_mem; + + Simulation simulation; + + StatisticsExt<> stat_len; + + StatisticsExt<> stat_in_tree_len; + + /** Local variable for update_rave(). + Reused for efficiency. */ + array was_played; + + /** Local variable for update_rave(). + Reused for efficiency. */ + array first_play; + + ~ThreadState(); + }; + + /** Thread in the parallel search. + The thread waits for a call to start_search(), then runs + SearchBase::search_loop()) with the thread-specific search state. + After start_search(), wait_search_finished() needs to called before + calling start_search() again or destructing this object. */ + class Thread + { + public: + typedef function SearchFunc; + + ThreadState thread_state; + + explicit Thread(SearchFunc& search_func); + + ~Thread(); + + void run(); + + void start_search(); + + void wait_search_finished(); + + private: + SearchFunc m_search_func; + + bool m_quit = false; + + bool m_start_search_flag = false; + + bool m_search_finished_flag = false; + + Barrier m_thread_ready{2}; + + mutex m_start_search_mutex; + + mutex m_search_finished_mutex; + + condition_variable m_start_search_cond; + + condition_variable m_search_finished_cond; + + unique_lock m_search_finished_lock{m_search_finished_mutex, + defer_lock}; + + thread m_thread; + + void thread_main(); + }; + + + /** @name Members that are used concurrently by all threads during the + lock-free multi-threaded search */ + /** @{ */ + + Tree m_tree; + + /** See get_root_val(). */ + array, max_players> m_root_val; + + LastGoodReply m_lgr; + + /** See get_nu_simulations(). */ + Atomic m_nu_simulations; + + /** @} */ // @name + + + unsigned m_nu_threads; + + Float m_expand_threshold = 0; + + Float m_expand_threshold_inc = 0; + + bool m_deterministic; + + bool m_reuse_subtree = true; + + bool m_reuse_tree = false; + + /** Player to play at the root node of the search. */ + PlayerInt m_player; + + /** Cached return value of get_nu_players() that stays constant during + a search. */ + PlayerInt m_nu_players; + + /** Time of last search. */ + double m_last_time; + + Float m_rave_parent_max = 50000; + + Float m_rave_child_max = 2000; + + Float m_rave_weight = 0.3f; + + /** Minimum simulations to perform in the current search. + This does not include the count of simulations reused from a subtree of + a previous search. */ + size_t m_min_simulations; + + /** Maximum simulations of current search. + This include the count of simulations reused from a subtree of a + previous search. */ + Float m_max_count; + + /** Maximum time of current search. */ + double m_max_time; + + TimeSource* m_time_source; + + Float m_exploration_constant; + + Timer m_timer; + + vector> m_threads; + + Tree m_tmp_tree; + +#if LIBBOARDGAME_DEBUG + AssertionHandler m_assertion_handler; +#endif + + + function m_callback; + + ArrayList m_followup_sequence; + + bool check_abort(const ThreadState& thread_state) const; + + LIBBOARDGAME_NOINLINE + bool check_abort_expensive(ThreadState& thread_state) const; + + bool check_cannot_change(ThreadState& thread_state, Float remaining) const; + + bool estimate_reused_root_val(Tree& tree, const Node& root, Float& value, + Float& count); + + bool expand_node(ThreadState& thread_state, const Node& node, + const Node*& best_child); + + void playout(ThreadState& thread_state); + + void play_in_tree(ThreadState& thread_state); + + bool prune(TimeSource& time_source, double time, Float prune_min_count, + Float& new_prune_min_count); + + void search_loop(ThreadState& thread_state); + + const Node* select_child(const Node& node); + + void update_lgr(ThreadState& thread_state); + + void update_rave(ThreadState& thread_state); + + void update_values(ThreadState& thread_state); +}; + + +template +SearchBase::ThreadState::~ThreadState() = default; + +template +SearchBase::Thread::Thread(SearchFunc& search_func) + : m_search_func(search_func) +{ } + +template +SearchBase::Thread::~Thread() +{ + if (! m_thread.joinable()) + return; + m_quit = true; + { + lock_guard lock(m_start_search_mutex); + m_start_search_flag = true; + } + m_start_search_cond.notify_one(); + m_thread.join(); +} + +template +void SearchBase::Thread::run() +{ + m_thread = thread(bind(&Thread::thread_main, this)); + m_thread_ready.wait(); +} + +template +void SearchBase::Thread::start_search() +{ + LIBBOARDGAME_ASSERT(m_thread.joinable()); + m_search_finished_lock.lock(); + { + lock_guard lock(m_start_search_mutex); + m_start_search_flag = true; + } + m_start_search_cond.notify_one(); +} + +template +void SearchBase::Thread::thread_main() +{ + unique_lock lock(m_start_search_mutex); + m_thread_ready.wait(); + while (true) + { + while (! m_start_search_flag) + m_start_search_cond.wait(lock); + m_start_search_flag = false; + if (m_quit) + break; + m_search_func(thread_state); + { + lock_guard lock(m_search_finished_mutex); + m_search_finished_flag = true; + } + m_search_finished_cond.notify_one(); + } +} + +template +void SearchBase::Thread::wait_search_finished() +{ + LIBBOARDGAME_ASSERT(m_thread.joinable()); + while (! m_search_finished_flag) + m_search_finished_cond.wait(m_search_finished_lock); + m_search_finished_flag = false; + m_search_finished_lock.unlock(); +} + + +#if LIBBOARDGAME_DEBUG +template +SearchBase::AssertionHandler::AssertionHandler( + const SearchBase& search) + : m_search(search) +{ +} + +template +SearchBase::AssertionHandler::~AssertionHandler() = default; + +template +void SearchBase::AssertionHandler::run() +{ + LIBBOARDGAME_LOG(m_search.dump()); +} +#endif // LIBBOARDGAME_DEBUG + + +template +SearchBase::SearchBase(unsigned nu_threads, size_t memory) + : m_tree(memory / 2, nu_threads), + m_nu_threads(nu_threads), + m_exploration_constant(0), + m_tmp_tree(memory / 2, m_nu_threads) +#if LIBBOARDGAME_DEBUG + , m_assertion_handler(*this) +#endif +{ } + +template +SearchBase::~SearchBase() = default; + +template +bool SearchBase::check_abort(const ThreadState& thread_state) const +{ +#if LIBBOARDGAME_DISABLE_LOG + LIBBOARDGAME_UNUSED(thread_state); +#endif + if (m_max_count > 0 && m_tree.get_root().get_visit_count() >= m_max_count) + { + LIBBOARDGAME_LOG_THREAD(thread_state, "Maximum count reached"); + return true; + } + return false; +} + +template +bool SearchBase::check_abort_expensive( + ThreadState& thread_state) const +{ + if (get_abort()) + { + LIBBOARDGAME_LOG_THREAD(thread_state, "Search aborted"); + return true; + } + static_assert(numeric_limits::radix == 2, ""); + auto count = m_tree.get_root().get_visit_count(); + if (count >= (size_t(1) << numeric_limits::digits) - 1) + { + LIBBOARDGAME_LOG_THREAD(thread_state, + "Max count supported by float exceeded"); + return true; + } + auto time = m_timer(); + if (! m_deterministic && time < 0.1) + // Simulations per second might be inaccurate for very small times + return false; + double simulations_per_sec; + if (time == 0) + simulations_per_sec = SearchParamConst::expected_sim_per_sec; + else + { + size_t nu_simulations = m_nu_simulations.load(memory_order_relaxed); + simulations_per_sec = double(nu_simulations) / time; + } + double remaining_time; + Float remaining_simulations; + if (m_max_count == 0) + { + // Search uses time limit + if (time > m_max_time) + { + LIBBOARDGAME_LOG_THREAD(thread_state, "Maximum time reached"); + return true; + } + remaining_time = m_max_time - time; + remaining_simulations = Float(remaining_time * simulations_per_sec); + } + else + { + // Search uses count limit + remaining_simulations = m_max_count - count; + remaining_time = remaining_simulations / simulations_per_sec; + } + if (thread_state.thread_id == 0 && m_callback) + m_callback(time, remaining_time); + if (check_cannot_change(thread_state, remaining_simulations)) + return true; + return false; +} + +template +bool SearchBase::check_cannot_change(ThreadState& thread_state, + Float remaining) const +{ +#if LIBBOARDGAME_DISABLE_LOG + LIBBOARDGAME_UNUSED(thread_state); +#endif + // select_final() selects move with highest number of wins. + Float max_wins = 0; + Float second_max = 0; + for (auto& i : m_tree.get_root_children()) + { + Float wins = i.get_value() * i.get_value_count(); + if (wins > max_wins) + { + second_max = max_wins; + max_wins = wins; + } + } + Float diff = max_wins - second_max; + if (SearchParamConst::use_unlikely_change) + { + // Weight remaining number of simulations with current global win rate, + // but not less than 10% + auto& root_val = m_root_val[m_player]; + Float win_rate; + if (root_val.get_count() > 100) + { + win_rate = root_val.get_mean(); + if (win_rate < 0.1f) + win_rate = 0.1f; + } + else + win_rate = 1; // Not enough statistics + if (diff < win_rate * remaining) + return false; + } + else if (diff < remaining) + return false; + LIBBOARDGAME_LOG_THREAD(thread_state, "Move will not change"); + return true; +} + +template +bool SearchBase::check_followup(ArrayList& sequence) +{ + LIBBOARDGAME_UNUSED(sequence); + return false; +} + +template +void SearchBase::create_threads() +{ + if (! multithread && m_nu_threads > 1) + throw runtime_error("libboardgame_mcts::Search was compiled" + " without support for multithreading"); + LIBBOARDGAME_LOG("Creating ", m_nu_threads, " threads"); + m_threads.clear(); + m_threads.reserve(m_nu_threads); + auto search_func = + static_cast( + bind(&SearchBase::search_loop, this, placeholders::_1)); + for (unsigned i = 0; i < m_nu_threads; ++i) + { + unique_ptr t(new Thread(search_func)); + auto& thread_state = t->thread_state; + thread_state.thread_id = i; + thread_state.state = create_state(); + for (auto& was_played : thread_state.was_played) + was_played = max_players; + if (i > 0) + t->run(); + m_threads.push_back(move(t)); + } +} + +#if LIBBOARDGAME_DEBUG +template +string SearchBase::dump() const +{ + ostringstream s; + for (unsigned i = 0; i < m_nu_threads; ++i) + { + s << "Thread state " << i << ":\n" + << get_state(i).dump(); + } + return s.str(); +} +#endif + +template +bool SearchBase::expand_node(ThreadState& thread_state, + const Node& node, + const Node*& best_child) +{ + auto& state = *thread_state.state; + auto thread_id = thread_state.thread_id; + typename Tree::NodeExpander expander(thread_id, m_tree, + SearchParamConst::child_min_count); + auto root_val = m_root_val[state.get_player()].get_mean(); + if (state.gen_children(expander, root_val)) + { + expander.link_children(m_tree, node); + best_child = expander.get_best_child(); + return true; + } + return false; +} + +template +inline auto SearchBase::get_expand_threshold() const -> Float +{ + return m_expand_threshold; +} + +template +inline auto SearchBase::get_expand_threshold_inc() const -> Float +{ + return m_expand_threshold_inc; +} + +template +inline size_t SearchBase::get_nu_simulations() const +{ + return m_nu_simulations; +} + +template +inline auto SearchBase::get_root_val(PlayerInt player) const +-> const StatisticsDirtyLockFree& +{ + LIBBOARDGAME_ASSERT(player < m_nu_players); + return m_root_val[player]; +} + +template +inline auto SearchBase::get_root_val() const +-> const StatisticsDirtyLockFree& +{ + return get_root_val(get_player()); +} + +template +inline auto SearchBase::get_root_visit_count() const -> Float +{ + return m_tree.get_root().get_visit_count(); +} + +template +inline auto SearchBase::get_rave_parent_max() const -> Float +{ + return m_rave_parent_max; +} + +template +inline auto SearchBase::get_rave_child_max() const -> Float +{ + return m_rave_child_max; +} + +template +inline auto SearchBase::get_rave_weight() const -> Float +{ + return m_rave_weight; +} + +template +inline bool SearchBase::get_reuse_subtree() const +{ + return m_reuse_subtree; +} + +template +inline bool SearchBase::get_reuse_tree() const +{ + return m_reuse_tree; +} + +template +inline S& SearchBase::get_state(unsigned thread_id) +{ + LIBBOARDGAME_ASSERT(thread_id < m_threads.size()); + return *m_threads[thread_id]->thread_state.state; +} + +template +inline const S& SearchBase::get_state(unsigned thread_id) const +{ + LIBBOARDGAME_ASSERT(thread_id < m_threads.size()); + return *m_threads[thread_id]->thread_state.state; +} + +template +inline TimeSource& SearchBase::get_time_source() +{ + LIBBOARDGAME_ASSERT(m_time_source != 0); + return *m_time_source; +} + +template +inline auto SearchBase::get_tree() const -> const Tree& +{ + return m_tree; +} + +template +void SearchBase::on_start_search(bool is_followup) +{ + // Default implementation does nothing + LIBBOARDGAME_UNUSED(is_followup); +} + +template +void SearchBase::playout(ThreadState& thread_state) +{ + auto& state = *thread_state.state; + state.start_playout(); + auto& simulation = thread_state.simulation; + auto& moves = simulation.moves; + auto nu_moves = moves.size(); + Move last = nu_moves > 0 ? moves[nu_moves - 1].move : Move::null(); + Move second_last = nu_moves > 1 ? moves[nu_moves - 2].move : Move::null(); + PlayerMove mv; + while (state.gen_playout_move(m_lgr, last, second_last, mv)) + { + state.play_playout(mv.move); + moves.push_back(mv); + second_last = last; + last = mv.move; + } +} + +template +void SearchBase::play_in_tree(ThreadState& thread_state) +{ + auto& state = *thread_state.state; + auto& simulation = thread_state.simulation; + simulation.nodes.resize(1); + simulation.moves.clear(); + auto& root = m_tree.get_root(); + auto node = &root; + Float expand_threshold = m_expand_threshold; + while (node->has_children()) + { + node = select_child(*node); + if (multithread && SearchParamConst::virtual_loss) + m_tree.add_value(*node, 0); + simulation.nodes.push_back(node); + Move mv = node->get_move(); + simulation.moves.push_back(PlayerMove(state.get_player(), mv)); + state.play_in_tree(mv); + expand_threshold += m_expand_threshold_inc; + } + state.finish_in_tree(); + if (node->get_visit_count() > expand_threshold) + { + if (! expand_node(thread_state, *node, node)) + thread_state.is_out_of_mem = true; + else if (node) + { + simulation.nodes.push_back(node); + Move mv = node->get_move(); + simulation.moves.push_back(PlayerMove(state.get_player(), mv)); + state.play_expanded_child(mv); + } + } + thread_state.stat_in_tree_len.add(double(simulation.moves.size())); +} + +template +string SearchBase::get_info() const +{ + auto& root = m_tree.get_root(); + if (m_threads.empty()) + return string(); + auto& thread_state = m_threads[0]->thread_state; + ostringstream s; + s << fixed << setprecision(2) << "Val: " << get_root_val().get_mean() + << setprecision(0) << ", ValCnt: " << get_root_val().get_count() + << ", VstCnt: " << get_root_visit_count() + << ", Sim: " << m_nu_simulations; + auto child = select_final(); + if (child && root.get_visit_count() > 0) + s << setprecision(1) << ", Chld: " + << (100 * child->get_visit_count() / root.get_visit_count()) + << '%'; + s << "\nNds: " << m_tree.get_nu_nodes() + << ", Tm: " << time_to_string(m_last_time) + << setprecision(0) << ", Sim/s: " + << (double(m_nu_simulations) / m_last_time) + << ", Len: " << thread_state.stat_len.to_string(true, 1, true) + << "\nDp: " << thread_state.stat_in_tree_len.to_string(true, 1, true) + << "\n"; + return s.str(); +} + +template +string SearchBase::get_info_ext() const +{ + return string(); +} + +template +bool SearchBase::prune(TimeSource& time_source, double time, + Float prune_min_count, + Float& new_prune_min_count) +{ +#if LIBBOARDGAME_DISABLE_LOG + LIBBOARDGAME_UNUSED(time); +#endif + Timer timer(time_source); + m_tmp_tree.clear(); + m_tree.copy_subtree(m_tmp_tree, m_tmp_tree.get_root(), m_tree.get_root(), + prune_min_count); + int percent = int(m_tmp_tree.get_nu_nodes() * 100 / m_tree.get_nu_nodes()); + LIBBOARDGAME_LOG("Pruning MinCnt: ", prune_min_count, ", AtTm: ", time, + ", Nds: ", m_tmp_tree.get_nu_nodes(), " (", percent, + "%), Tm: ", timer()); + m_tree.swap(m_tmp_tree); + if (percent > 50) + { + if (prune_min_count >= 0.5 * numeric_limits::max()) + return false; + new_prune_min_count = prune_min_count * 2; + return true; + } + else + { + new_prune_min_count = prune_min_count; + return true; + } +} + +/** Estimate the value and count of a root node from its children. + After reusing a subtree, we don't know the value of the root because nodes + only store the value of moves. To estimate the root value, we use the child + with the highest visit count. */ +template +bool SearchBase::estimate_reused_root_val(Tree& tree, + const Node& root, + Float& value, Float& count) +{ + const Node* best = nullptr; + Float max_count = 0; + for (auto& i : tree.get_children(root)) + if (i.get_visit_count() > max_count) + { + best = &i; + max_count = i.get_visit_count(); + } + if (! best) + return false; + value = best->get_value(); + count = best->get_value_count(); + return count > 0; +} + +template +bool SearchBase::search(Move& mv, Float max_count, + size_t min_simulations, double max_time, + TimeSource& time_source) +{ + if (m_nu_threads != m_threads.size()) + create_threads(); + m_deterministic = RandomGenerator::has_global_seed(); + bool is_followup = check_followup(m_followup_sequence); + on_start_search(is_followup); + if (max_count > 0) + // A fixed number of simulations means that no time limit is used, but + // max_time is still used at some places in the code, so we set it to + // infinity + max_time = numeric_limits::max(); + m_player = get_player(); + m_nu_players = get_nu_players(); + bool clear_tree = true; + bool is_same = false; + if (is_followup && m_followup_sequence.empty()) + { + is_same = true; + is_followup = false; + } + if (is_same || (is_followup && m_followup_sequence.size() <= m_nu_players)) + { + // Use root_val from last search but with a count of max. 100 + for (PlayerInt i = 0; i < m_nu_players; ++i) + if (m_root_val[i].get_count() > 100) + m_root_val[i].init(m_root_val[i].get_mean(), 100); + } + else + for (PlayerInt i = 0; i < m_nu_players; ++i) + m_root_val[i].init(SearchParamConst::tie_value, 1); + if ((m_reuse_subtree && is_followup) || (m_reuse_tree && is_same)) + { + size_t tree_nodes = m_tree.get_nu_nodes(); + if (m_followup_sequence.empty()) + { + if (tree_nodes > 1) + LIBBOARDGAME_LOG("Reusing all ", tree_nodes, "nodes (count=", + m_tree.get_root().get_visit_count(), ")"); + } + else + { + Timer timer(time_source); + m_tmp_tree.clear(); + auto node = find_node(m_tree, m_followup_sequence); + if (node) + { + m_tree.extract_subtree(m_tmp_tree, *node); + auto& tmp_tree_root = m_tmp_tree.get_root(); + if (! is_same) + { + Float value, count; + if (estimate_reused_root_val(m_tmp_tree, tmp_tree_root, + value, count)) + m_root_val[m_player].add(value, count); + } + size_t tmp_tree_nodes = m_tmp_tree.get_nu_nodes(); + if (tree_nodes > 1 && tmp_tree_nodes > 1) + { + double time = timer(); + LIBBOARDGAME_LOG("Reusing ", tmp_tree_nodes, " nodes (", + std::fixed, setprecision(1), + 100 * double(tmp_tree_nodes) + / double(tree_nodes), + "% tm=", setprecision(4), time, ")"); + m_tree.swap(m_tmp_tree); + clear_tree = false; + max_time -= time; + if (max_time < 0) + max_time = 0; + } + } + } + } + if (clear_tree) + m_tree.clear(); + + m_timer.reset(time_source); + m_time_source = &time_source; + if (SearchParamConst::use_lgr && ! is_followup) + m_lgr.init(m_nu_players); + for (auto& i : m_threads) + { + auto& thread_state = i->thread_state; + thread_state.stat_len.clear(); + thread_state.stat_in_tree_len.clear(); + thread_state.state->start_search(); + } + m_max_count = max_count; + m_min_simulations = min_simulations; + m_max_time = max_time; + m_nu_simulations.store(0); + Float prune_min_count = SearchParamConst::prune_count_start; + + // Don't use multi-threading for very short searches (less than 0.5s). + auto reused_count = m_tree.get_root().get_visit_count(); + unsigned nu_threads = m_nu_threads; + double expected_time; + if (max_count > 0) + expected_time = + (max_count - reused_count) + / SearchParamConst::expected_sim_per_sec; + else + expected_time = max_time; + if (nu_threads > 1 && expected_time < 0.5) + { + LIBBOARDGAME_LOG("Using single-threading for short search"); + nu_threads = 1; + } + + auto& thread_state_0 = m_threads[0]->thread_state; + auto& root = m_tree.get_root(); + if (! root.has_children()) + { + const Node* best_child; + thread_state_0.state->start_simulation(0); + thread_state_0.state->finish_in_tree(); + expand_node(thread_state_0, root, best_child); + } + + if (root.get_nu_children() == 0) + LIBBOARDGAME_LOG("No legal moves at root"); + else if (root.get_nu_children() == 1 && min_simulations == 0) + LIBBOARDGAME_LOG("Root has only one child"); + else + while (true) + { + for (unsigned i = 1; i < nu_threads; ++i) + m_threads[i]->start_search(); + search_loop(thread_state_0); + for (unsigned i = 1; i < nu_threads; ++i) + m_threads[i]->wait_search_finished(); + bool is_out_of_mem = false; + for (unsigned i = 0; i < nu_threads; ++i) + if (m_threads[i]->thread_state.is_out_of_mem) + { + is_out_of_mem = true; + break; + } + if (! is_out_of_mem) + break; + double time = m_timer(); + prune(time_source, time, prune_min_count, prune_min_count); + } + + m_last_time = m_timer(); + LIBBOARDGAME_LOG(get_info()); + bool result = select_move(mv); + m_time_source = nullptr; + return result; +} + +template +void SearchBase::search_loop(ThreadState& thread_state) +{ + auto& state = *thread_state.state; + auto& simulation = thread_state.simulation; + simulation.nodes.assign(&m_tree.get_root()); + simulation.moves.clear(); + double time_interval = 0.1; + if (m_max_count == 0 && m_max_time < 1) + time_interval = 0.1 * m_max_time; + IntervalChecker expensive_abort_checker( + *m_time_source, time_interval, + bind(&SearchBase::check_abort_expensive, this, + ref(thread_state))); + if (m_deterministic) + { + unsigned interval = + static_cast( + max(1.0, SearchParamConst::expected_sim_per_sec / 5.0)); + expensive_abort_checker.set_deterministic(interval); + } + while (true) + { + thread_state.is_out_of_mem = false; + if ((check_abort(thread_state) || expensive_abort_checker()) + && m_nu_simulations >= m_min_simulations) + break; + state.start_simulation(m_nu_simulations.fetch_add(1)); + play_in_tree(thread_state); + if (thread_state.is_out_of_mem) + break; + playout(thread_state); + state.evaluate_playout(simulation.eval); + thread_state.stat_len.add(double(simulation.moves.size())); + update_values(thread_state); + if (SearchParamConst::rave) + update_rave(thread_state); + if (SearchParamConst::use_lgr) + update_lgr(thread_state); + } +} + +template +inline auto SearchBase::select_child(const Node& node) -> const Node* +{ + auto children = m_tree.get_children(node); + LIBBOARDGAME_ASSERT(! children.empty()); + auto parent_count = node.get_visit_count(); + Float bias_factor = m_exploration_constant * sqrt(parent_count); + static_assert(SearchParamConst::child_min_count > 0, ""); + auto bias_limit = bias_factor / SearchParamConst::child_min_count; + auto i = children.begin(); + auto value = i->get_value() + bias_factor / i->get_value_count(); + auto best_value = value; + auto best_child = i; + auto limit = best_value - bias_limit; + while (++i != children.end()) + { + value = i->get_value(); + if (value <= limit) + continue; + value += bias_factor / i->get_value_count(); + if (value > best_value) + { + best_value = value; + best_child = i; + limit = best_value - bias_limit; + } + } + return best_child; +} + +template +auto SearchBase::select_final() const-> const Node* +{ + // Select the child with the highest number of wins + auto children = m_tree.get_children(m_tree.get_root()); + if (children.empty()) + return nullptr; + auto i = children.begin(); + auto best_child = i; + auto max_wins = i->get_value_count() * i->get_value(); + while (++i != children.end()) + { + auto wins = i->get_value_count() * i->get_value(); + if (wins > max_wins) + { + max_wins = wins; + best_child = i; + } + } + return best_child; +} + +template +bool SearchBase::select_move(Move& mv) const +{ + auto child = select_final(); + if (child) + { + mv = child->get_move(); + return true; + } + else + return false; +} + +template +void SearchBase::set_callback(function callback) +{ + m_callback = callback; +} + +template +void SearchBase::set_expand_threshold(Float n) +{ + m_expand_threshold = n; +} + +template +void SearchBase::set_expand_threshold_inc(Float n) +{ + m_expand_threshold_inc = n; +} + +template +void SearchBase::set_rave_parent_max(Float n) +{ + m_rave_parent_max = n; +} + +template +void SearchBase::set_rave_child_max(Float n) +{ + m_rave_child_max = n; +} + +template +void SearchBase::set_rave_weight(Float v) +{ + m_rave_weight = v; +} + +template +void SearchBase::set_reuse_subtree(bool enable) +{ + m_reuse_subtree = enable; +} + +template +void SearchBase::set_reuse_tree(bool enable) +{ + m_reuse_tree = enable; +} + +template +void SearchBase::update_lgr(ThreadState& thread_state) +{ + const auto& simulation = thread_state.simulation; + auto& eval = simulation.eval; + auto max_eval = eval[0]; + for (PlayerInt i = 1; i < m_nu_players; ++i) + max_eval = max(eval[i], max_eval); + array is_winner; + for (PlayerInt i = 0; i < m_nu_players; ++i) + // Note: this handles a draw as a win. Without additional information + // we cannot make a good decision how to handle draws and some + // experiments in Blokus Duo showed (with low confidence) that treating + // them as a win for both players is slightly better than treating them + // as a loss for both. + is_winner[i] = (eval[i] == max_eval); + auto& moves = simulation.moves; + auto nu_moves = moves.size(); + Move last = moves.get_unchecked(0).move; + Move second_last = Move::null(); + for (unsigned i = 1; i < nu_moves; ++i) + { + PlayerMove reply = moves[i]; + PlayerInt player = reply.player; + Move mv = reply.move; + if (is_winner[player]) + m_lgr.store(player, last, second_last, mv); + else + m_lgr.forget(player, last, second_last, mv); + second_last = last; + last = mv; + } +} + +template +void SearchBase::update_rave(ThreadState& thread_state) +{ + const auto& state = *thread_state.state; + auto& moves = thread_state.simulation.moves; + auto nu_moves = static_cast(moves.size()); + if (nu_moves == 0) + return; + auto& was_played = thread_state.was_played; + auto& first_play = thread_state.first_play; + auto& nodes = thread_state.simulation.nodes; + unsigned nu_nodes = static_cast(nodes.size()); + unsigned i = nu_moves - 1; + // nu_nodes is at least 2 (including root) because the case of no legal + // moves at the root is already handled before running any simulations. + LIBBOARDGAME_ASSERT(nu_nodes > 1); + + // Fill was_played and first_play with information from playout moves + for ( ; i >= nu_nodes - 1; --i) + { + auto mv = moves[i]; + if (state.skip_rave(mv.move)) + continue; + was_played[mv.move.to_int()] = mv.player; + first_play[mv.move.to_int()] = i; + } + + // Add RAVE values to children of nodes of current simulation + while (true) + { + const auto node = nodes[i]; + if (node->get_visit_count() > m_rave_parent_max) + break; + auto mv = moves[i]; + auto player = mv.player; + Float dist_factor; + if (SearchParamConst::rave_dist_weighting) + dist_factor = 1 / static_cast(nu_moves - i); + auto children = m_tree.get_children(*node); + LIBBOARDGAME_ASSERT(! children.empty()); + auto it = children.begin(); + do + { + auto mv = it->get_move(); + if (was_played[mv.to_int()] != player + || it->get_value_count() > m_rave_child_max) + continue; + auto first = first_play[mv.to_int()]; + LIBBOARDGAME_ASSERT(first > i); + Float weight = m_rave_weight; + if (SearchParamConst::rave_dist_weighting) + weight *= 1 - static_cast(first - i) * dist_factor; + m_tree.add_value(*it, thread_state.simulation.eval[player], weight); + } + while (++it != children.end()); + if (i == 0) + break; + if (! state.skip_rave(mv.move)) + { + was_played[mv.move.to_int()] = player; + first_play[mv.move.to_int()] = i; + } + --i; + } + + // Reset was_played + while (++i < nu_moves) + was_played[moves[i].move.to_int()] = max_players; +} + +template +void SearchBase::update_values(ThreadState& thread_state) +{ + const auto& simulation = thread_state.simulation; + auto& nodes = simulation.nodes; + auto& eval = simulation.eval; + unsigned nu_nodes = static_cast(nodes.size()); + m_tree.inc_visit_count(*nodes[0]); + for (unsigned i = 1; i < nu_nodes; ++i) + { + auto& node = *nodes[i]; + auto mv = simulation.moves[i - 1]; + if (multithread && SearchParamConst::virtual_loss) + // Note that this could become problematic if the number of threads + // is large. The lock-free algorithm intentionally ignores lost or + // partial updates to run faster. But the probability that adding + // a virtual loss is lost is not the same as that its removal is + // lost because the removal is done in this function with many + // calls to add_value() but the adding is done in play_in_tree(). + // This could introduce a systematic error. + m_tree.add_value_remove_loss(node, eval[mv.player]); + else + m_tree.add_value(node, eval[mv.player]); + m_tree.inc_visit_count(node); + } + for (PlayerInt i = 0; i < m_nu_players; ++i) + m_root_val[i].add(eval[i]); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_mcts + +#endif // LIBBOARDGAME_MCTS_SEARCH_BASE_H diff --git a/src/libboardgame_mcts/Tree.h b/src/libboardgame_mcts/Tree.h new file mode 100644 index 0000000..ba5f0ed --- /dev/null +++ b/src/libboardgame_mcts/Tree.h @@ -0,0 +1,474 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_mcts/Tree.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_MCTS_TREE_H +#define LIBBOARDGAME_MCTS_TREE_H + +#include +#include +#include "Node.h" +#include "libboardgame_util/Abort.h" +#include "libboardgame_util/IntervalChecker.h" + +namespace libboardgame_mcts { + +using namespace std; +using libboardgame_util::get_abort; +using libboardgame_util::IntervalChecker; + +//----------------------------------------------------------------------------- + +/** %Tree for Monte-Carlo tree search. + The nodes can be modified only through member functions of this class, + so that it can guarantee an intact tree structure. The user has access to + all nodes, but only as const references.

+ The tree uses separate parts of the node storage for different threads, + so it can be used without locking in multi-threaded search. Not all + functions are thread-safe, only the ones that are used during a search + (e.g. expanding a node is thread-safe, but clear() is not) */ +template +class Tree +{ + struct ThreadStorage; + + friend class NodeExpander; + +public: + typedef N Node; + + typedef typename Node::Move Move; + + typedef typename Node::Float Float; + + /** Range for iterating over the children of a node. */ + class Children + { + public: + Children(const Tree& tree, const Node& node) + { + auto nu_children = node.get_nu_children(); + m_begin = (nu_children != 0 ? + &tree.get_node(node.get_first_child()) : nullptr); + m_end = m_begin + nu_children; + } + + const Node* begin() const + { + return m_begin; + } + + const Node* end() const + { + return m_end; + } + + bool empty() const + { + return m_begin == nullptr; + } + + private: + const Node* m_begin; + + const Node* m_end; + }; + + + /** Helper class that is passed to the search state during node expansion. + This class allows the search state to directly create children of a + node at the node expansion, so that copying to a temporary move list + is not necessary, but avoids that the search needs to expose a + non-const reference to the tree to the state. */ + class NodeExpander + { + public: + /** Constructor. + @param thread_id + @param tree + @param child_min_count The minimum count used for initializing + children. Used only in debug mode to assert that the children + are really initialized with a minimum count as declared with + SearchParamConst::child_min_count. */ + NodeExpander(unsigned thread_id, Tree& tree, Float child_min_count); + + /** Check if the tree still has the capacity for a given number + of children. */ + bool check_capacity(unsigned short nu_children) const; + + /** Add new child. + It needs to be checked first with check_capacity() that the tree + has enough capacity. */ + void add_child(const Move& mv, Float value, Float count); + + /** Link the children to the parent node. */ + void link_children(Tree& tree, const Node& node); + + /** Return the node to play after the node expansion. + This returns the child with the highest value if prior knowledge + was used, or the first child, or null if no children. This can be + used for avoiding and extra iteration over the children when + selecting a child after a node expansion. */ + const Node* get_best_child() const; + + private: + ThreadStorage& m_thread_storage; + + Float m_best_value = -numeric_limits::max(); + + const Node* m_first_child; + + const Node* m_best_child; + +#if LIBBOARDGAME_DEBUG + Float m_child_min_count; +#endif + }; + + Tree(size_t memory, unsigned nu_threads); + + ~Tree(); + + /** Remove all nodes but the root node. */ + void clear(); + + const Node& get_root() const; + + Children get_children(const Node& node) const + { + return Children(*this, node); + } + + Children get_root_children() const + { + return get_children(get_root()); + } + + size_t get_nu_nodes() const; + + const Node& get_node(NodeIdx i) const; + + void link_children(const Node& node, const Node* first_child, + unsigned short nu_children); + + void add_value(const Node& node, Float v); + + void add_value(const Node& node, Float v, Float weight); + + void add_value_remove_loss(const Node& node, Float v); + + void inc_visit_count(const Node& node); + + void swap(Tree& tree); + + /** Extract a subtree. + Note that you still have to re-initialize the value of the subtree + after the extraction because the value of the root node and the values + of inner nodes have a different meaning. + @pre Target tree is empty (! target.get_root().has_children()) + @param target The target tree + @param node The root node of the subtree. */ + void extract_subtree(Tree& target, const Node& node) const; + + /** Copy a subtree. + The caller is responsible that the trees have the same number of + maximum nodes and that the target tree has room for the subtree. + @param target The target tree + @param target_node The target node + @param node The root node of the subtree. + @param min_count Don't copy subtrees of nodes below this count */ + void copy_subtree(Tree& target, const Node& target_node, const Node& node, + Float min_count) const; + +private: + struct ThreadStorage + { + Node* begin; + + Node* end; + + Node* next; + }; + + + unique_ptr m_nodes; + + unique_ptr m_thread_storage; + + unsigned m_nu_threads; + + size_t m_max_nodes; + + size_t m_nodes_per_thread; + + + bool contains(const Node& node) const; + + void copy_recurse(Tree& target, const Node& target_node, const Node& node, + Float min_count) const; + + unsigned get_thread_storage(const Node& node) const; + + Node& non_const(const Node& node) const; +}; + +template +inline Tree::NodeExpander::NodeExpander(unsigned thread_id, Tree& tree, + Float child_min_count) + : m_thread_storage(tree.m_thread_storage[thread_id]), + m_first_child(m_thread_storage.next), + m_best_child(nullptr) +{ + LIBBOARDGAME_ASSERT(thread_id < tree.m_nu_threads); +#if LIBBOARDGAME_DEBUG + m_child_min_count = child_min_count; +#else + LIBBOARDGAME_UNUSED(child_min_count); +#endif +} + +template +inline void Tree::NodeExpander::add_child(const Move& mv, Float value, + Float count) +{ + // -numeric_limits::max() ist init value for m_best_value + LIBBOARDGAME_ASSERT(value > -numeric_limits::max()); + LIBBOARDGAME_ASSERT(count >= m_child_min_count); + auto& next = m_thread_storage.next; + LIBBOARDGAME_ASSERT(next < m_thread_storage.end); + next->init(mv, value, count); + if (value > m_best_value) + { + m_best_child = next; + m_best_value = value; + } + ++next; +} + +template +inline bool Tree::NodeExpander::check_capacity( + unsigned short nu_children) const +{ + return m_thread_storage.end - m_thread_storage.next >= nu_children; +} + +template +inline auto Tree::NodeExpander::get_best_child() const -> const Node* +{ + return m_best_child; +} + +template +inline auto Tree::get_node(NodeIdx i) const -> const Node& +{ + return m_nodes[i]; +} + +template +inline void Tree::NodeExpander::link_children(Tree& tree, const Node& node) +{ + auto nu_children = + static_cast(m_thread_storage.next - m_first_child); + tree.link_children(node, m_first_child, nu_children); +} + + +template +Tree::Tree(size_t memory, unsigned nu_threads) + : m_nu_threads(nu_threads) +{ + size_t max_nodes = memory / sizeof(Node); + // It doesn't make sense to set max_nodes higher than what can be accessed + // with NodeIdx + max_nodes = + min(max_nodes, static_cast(numeric_limits::max())); + if (max_nodes == 0) + // We need at least the root node (for useful searches we need of + // course also children, but a root node is the minimum requirement to + // avoid crashing). + max_nodes = 1; + m_max_nodes = max_nodes; + m_nodes.reset(new Node[max_nodes]); + m_thread_storage.reset(new ThreadStorage[m_nu_threads]); + m_nodes_per_thread = max_nodes / m_nu_threads; + for (unsigned i = 0; i < m_nu_threads; ++i) + { + auto& thread_storage = m_thread_storage[i]; + thread_storage.begin = m_nodes.get() + i * m_nodes_per_thread; + thread_storage.end = thread_storage.begin + m_nodes_per_thread; + } + clear(); +} + +template +Tree::~Tree() = default; + +template +inline void Tree::add_value(const Node& node, Float v) +{ + non_const(node).add_value(v); +} + +template +inline void Tree::add_value(const Node& node, Float v, Float weight) +{ + non_const(node).add_value(v, weight); +} + +template +void Tree::clear() +{ + m_thread_storage[0].next = m_thread_storage[0].begin + 1; + for (unsigned i = 1; i < m_nu_threads; ++i) + m_thread_storage[i].next = m_thread_storage[i].begin; + m_nodes[0].init_root(); +} + +template +bool Tree::contains(const Node& node) const +{ + return &node >= m_nodes.get() && &node < m_nodes.get() + m_max_nodes; +} + +template +void Tree::copy_subtree(Tree& target, const Node& target_node, + const Node& node, Float min_count) const +{ + target.non_const(target_node).copy_data_from(node); + if (node.has_children()) + copy_recurse(target, target_node, node, min_count); + else + target.non_const(target_node).unlink_children_st(); +} + +template +void Tree::copy_recurse(Tree& target, const Node& target_node, + const Node& node, Float min_count) const +{ + LIBBOARDGAME_ASSERT(target.m_max_nodes == m_max_nodes); + LIBBOARDGAME_ASSERT(target.m_nu_threads == m_nu_threads); + LIBBOARDGAME_ASSERT(contains(node)); + auto nu_children = node.get_nu_children(); + auto& first_child = get_node(node.get_first_child()); + // Create target children in the equivalent thread storage as in source. + // This ensures that the thread storage will not overflow (because the + // trees have identical nu_threads/max_nodes) + ThreadStorage& thread_storage = + target.m_thread_storage[get_thread_storage(first_child)]; + auto target_child = thread_storage.next; + auto target_first_child = + static_cast(target_child - target.m_nodes.get()); + target.non_const(target_node).link_children_st(target_first_child, + nu_children); + thread_storage.next += nu_children; + // Without the extra () around thread_storage.next in the following + // assert, GCC 4.7.2 gives the error: parse error in template argument list + LIBBOARDGAME_ASSERT((thread_storage.next) < thread_storage.end); + auto end = &first_child + node.get_nu_children(); + for (auto i = &first_child; i != end; ++i, ++target_child) + { + target_child->copy_data_from(*i); + if (! i->has_children() || i->get_visit_count() < min_count) + { + target_child->unlink_children_st(); + continue; + } + copy_recurse(target, *target_child, *i, min_count); + } +} + +template +void Tree::extract_subtree(Tree& target, const Node& node) const +{ + LIBBOARDGAME_ASSERT(contains(node)); + LIBBOARDGAME_ASSERT(&target != this); + LIBBOARDGAME_ASSERT(target.m_max_nodes == m_max_nodes); + LIBBOARDGAME_ASSERT(! target.get_root().has_children()); + copy_subtree(target, target.m_nodes[0], node, 0); +} + +template +size_t Tree::get_nu_nodes() const +{ + size_t result = 0; + for (unsigned i = 0; i < m_nu_threads; ++i) + { + auto& thread_storage = m_thread_storage[i]; + result += thread_storage.next - thread_storage.begin; + } + return result; +} + +template +inline auto Tree::get_root() const -> const Node& +{ + return m_nodes[0]; +} + +/** Get the thread storage a node belongs to. */ +template +inline unsigned Tree::get_thread_storage(const Node& node) const +{ + size_t diff = &node - m_nodes.get(); + return static_cast(diff / m_nodes_per_thread); +} + +template +inline void Tree::inc_visit_count(const Node& node) +{ + non_const(node).inc_visit_count(); +} + +template +inline void Tree::link_children(const Node& node, const Node* first_child, + unsigned short nu_children) +{ + NodeIdx first_child_idx = static_cast(first_child - m_nodes.get()); + LIBBOARDGAME_ASSERT(first_child_idx > 0); + LIBBOARDGAME_ASSERT(first_child_idx < m_max_nodes); + non_const(node).link_children(first_child_idx, nu_children); +} + +/** Convert a const reference to node from user to a non-const reference. + The user has only read access to the nodes, because the tree guarantees + the validity of the tree structure. */ +template +inline auto Tree::non_const(const Node& node) const -> Node& +{ + LIBBOARDGAME_ASSERT(contains(node)); + return const_cast(node); +} + +template +inline void Tree::add_value_remove_loss(const Node& node, Float v) +{ + non_const(node).add_value_remove_loss(v); +} + +template +void Tree::swap(Tree& tree) +{ + // Reminder to update this function when the class gets additional members + struct Dummy + { + unsigned m_nu_threads; + size_t m_max_nodes; + size_t m_nodes_per_thread; + unique_ptr m_thread_storage; + unique_ptr m_nodes; + }; + static_assert(sizeof(Tree) == sizeof(Dummy), ""); + std::swap(m_nu_threads, tree.m_nu_threads); + std::swap(m_max_nodes, tree.m_max_nodes); + std::swap(m_nodes_per_thread, tree.m_nodes_per_thread); + m_thread_storage.swap(tree.m_thread_storage); + m_nodes.swap(tree.m_nodes); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_mcts + +#endif // LIBBOARDGAME_MCTS_TREE_H diff --git a/src/libboardgame_mcts/TreeUtil.h b/src/libboardgame_mcts/TreeUtil.h new file mode 100644 index 0000000..6b64efe --- /dev/null +++ b/src/libboardgame_mcts/TreeUtil.h @@ -0,0 +1,41 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_mcts/TreeUtil.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_MCTS_TREE_UTIL_H +#define LIBBOARDGAME_MCTS_TREE_UTIL_H + +#include "Tree.h" + +namespace libboardgame_mcts { +namespace tree_util { + +//----------------------------------------------------------------------------- + +template +const N* find_child(const Tree& tree, const N& node, typename N::Move mv) +{ + for (auto& i : tree.get_children(node)) + if (i.get_move() == mv) + return &i; + return nullptr; +} + +template +const N* find_node(const Tree& tree, const S& sequence) +{ + auto node = &tree.get_root(); + for (auto mv : sequence) + if (! ((node = find_child(tree, *node, mv)))) + break; + return node; +} + +//----------------------------------------------------------------------------- + +} // namespace tree_util +} // namespace libboardgame_mcts + +#endif // LIBBOARDGAME_MCTS_TREE_UTIL_H diff --git a/src/libboardgame_sgf/CMakeLists.txt b/src/libboardgame_sgf/CMakeLists.txt new file mode 100644 index 0000000..0cb8af9 --- /dev/null +++ b/src/libboardgame_sgf/CMakeLists.txt @@ -0,0 +1,20 @@ +add_library(boardgame_sgf STATIC + InvalidPropertyValue.h + InvalidTree.h + MissingProperty.h + MissingProperty.cpp + Reader.h + Reader.cpp + SgfNode.h + SgfNode.cpp + SgfTree.h + SgfTree.cpp + SgfUtil.h + SgfUtil.cpp + TreeReader.h + TreeReader.cpp + TreeWriter.h + TreeWriter.cpp + Writer.h + Writer.cpp +) diff --git a/src/libboardgame_sgf/InvalidPropertyValue.h b/src/libboardgame_sgf/InvalidPropertyValue.h new file mode 100644 index 0000000..7fc7e54 --- /dev/null +++ b/src/libboardgame_sgf/InvalidPropertyValue.h @@ -0,0 +1,50 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/InvalidPropertyValue.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_SGF_INVALID_PROPERTY_VALUE_H +#define LIBBOARDGAME_SGF_INVALID_PROPERTY_VALUE_H + +#include "InvalidTree.h" + +#include "sstream" + +namespace libboardgame_sgf { + +using namespace std; + +//----------------------------------------------------------------------------- + +class InvalidPropertyValue + : public InvalidTree +{ +public: + template + InvalidPropertyValue(const string& id, const T& value); + +private: + template + static string get_message(const string& id, const T& value); +}; + +template +InvalidPropertyValue::InvalidPropertyValue(const string& id, const T& value) + : InvalidTree(get_message(id, value)) +{ +} + +template +string InvalidPropertyValue::get_message(const string& id, const T& value) +{ + ostringstream msg; + msg << "Invalid value '" << value << " for SGF property '" << id << "'"; + return msg.str(); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf + +#endif // LIBBOARDGAME_SGF_INVALID_PROPERTY_VALUE_H diff --git a/src/libboardgame_sgf/InvalidTree.h b/src/libboardgame_sgf/InvalidTree.h new file mode 100644 index 0000000..1bda509 --- /dev/null +++ b/src/libboardgame_sgf/InvalidTree.h @@ -0,0 +1,37 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/InvalidTree.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_SGF_INVALID_TREE_H +#define LIBBOARDGAME_SGF_INVALID_TREE_H + +#include + +namespace libboardgame_sgf { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Exception indication a semantic error in the tree. + This exception is used for semantic errors in SGF trees. If a SGF tree + is loaded from an external file, it is usually only checked for + (game-independent) syntax errors, but not for semantic errors (e.g. illegal + moves) because that would be too expensive when loading large trees and + not allow the user to partially use a tree if there is an error only in + some variations. As a consequence, functions that use the tree may cause + errors later (e.g. when trying to update the game state to a node in the + tree). In this case, they should throw InvalidTree. */ +class InvalidTree + : public runtime_error +{ + using runtime_error::runtime_error; +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf + +#endif // LIBBOARDGAME_SGF_INVALID_TREE_H diff --git a/src/libboardgame_sgf/MissingProperty.cpp b/src/libboardgame_sgf/MissingProperty.cpp new file mode 100644 index 0000000..eeb3a4b --- /dev/null +++ b/src/libboardgame_sgf/MissingProperty.cpp @@ -0,0 +1,29 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/MissingProperty.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "MissingProperty.h" + +namespace libboardgame_sgf { + +//----------------------------------------------------------------------------- + +MissingProperty::MissingProperty(const string& message) + : InvalidTree("Missing SGF property: " + message) +{ +} + +MissingProperty::MissingProperty(const string& id, const string& message) + : InvalidTree("Missing SGF property '" + id + ": " + message) +{ +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf diff --git a/src/libboardgame_sgf/MissingProperty.h b/src/libboardgame_sgf/MissingProperty.h new file mode 100644 index 0000000..83fbb3c --- /dev/null +++ b/src/libboardgame_sgf/MissingProperty.h @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/MissingProperty.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_SGF_MISSING_PROPERTY_H +#define LIBBOARDGAME_SGF_MISSING_PROPERTY_H + +#include "InvalidTree.h" + +namespace libboardgame_sgf { + +using namespace std; + +//----------------------------------------------------------------------------- + +class MissingProperty + : public InvalidTree +{ +public: + explicit MissingProperty(const string& message); + + MissingProperty(const string& id, const string& message); +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf + +#endif // LIBBOARDGAME_SGF_MISSING_PROPERTY_H diff --git a/src/libboardgame_sgf/Reader.cpp b/src/libboardgame_sgf/Reader.cpp new file mode 100644 index 0000000..f0004d7 --- /dev/null +++ b/src/libboardgame_sgf/Reader.cpp @@ -0,0 +1,261 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/Reader.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Reader.h" + +#include +#include +#include +#include "libboardgame_util/Assert.h" +#include "libboardgame_util/Unused.h" + +namespace libboardgame_sgf { + +//----------------------------------------------------------------------------- + +namespace { + +/** Replacement for std::isspace() that returns true only for whitespaces + in the ASCII range. */ +bool is_ascii_space(int c) +{ + return c >= 0 && c < 128 && isspace(c); +} + +} // namespace + +//----------------------------------------------------------------------------- + +Reader::Reader() = default; + +Reader::~Reader() = default; + +void Reader::consume_char(char expected) +{ + LIBBOARDGAME_UNUSED_IF_NOT_DEBUG(expected); + char c = read_char(); + LIBBOARDGAME_UNUSED_IF_NOT_DEBUG(c); + LIBBOARDGAME_ASSERT(c == expected); +} + +void Reader::consume_whitespace() +{ + while (is_ascii_space(peek())) + m_in->get(); +} + +void Reader::on_begin_node(bool is_root) +{ + // Default implementation does nothing + LIBBOARDGAME_UNUSED(is_root); +} + +void Reader::on_begin_tree(bool is_root) +{ + // Default implementation does nothing + LIBBOARDGAME_UNUSED(is_root); +} + +void Reader::on_end_node() +{ + // Default implementation does nothing +} + +void Reader::on_end_tree(bool is_root) +{ + // Default implementation does nothing + LIBBOARDGAME_UNUSED(is_root); +} + +void Reader::on_property(const string& id, const vector& values) +{ + // Default implementation does nothing + LIBBOARDGAME_UNUSED(id); + LIBBOARDGAME_UNUSED(values); +} + +char Reader::peek() +{ + int c = m_in->peek(); + if (c == EOF) + throw ReadError("Unexpected end of input"); + return char(c); +} + +void Reader::read(istream& in, bool check_single_tree, + bool* more_game_trees_left) +{ + m_in = ∈ + m_is_in_main_variation = true; + consume_whitespace(); + read_tree(true); + while (true) + { + int c = m_in->peek(); + if (c == EOF) + { + if (more_game_trees_left) + *more_game_trees_left = false; + return; + } + else if (c == '(') + { + if (check_single_tree) + throw ReadError("Input has multiple game trees"); + else + { + if (more_game_trees_left) + *more_game_trees_left = true; + return; + } + } + else if (is_ascii_space(c)) + m_in->get(); + else + throw ReadError("Extra characters after end of tree."); + } +} + +void Reader::read(const string& file) +{ + ifstream in(file); + if (! in) + throw ReadError("Could not open '" + file + "'"); + try + { + read(in, true); + } + catch (const ReadError& e) + { + throw ReadError("Could not read '" + file + "': " + e.what()); + } +} + +char Reader::read_char() +{ + int c = m_in->get(); + if (c == EOF) + throw ReadError("Unexpected end of SGF stream"); + if (c == '\r') + { + // Convert CR+LF or single CR into LF + if (peek() == '\n') + m_in->get(); + return '\n'; + } + return char(c); +} + +void Reader::read_expected(char expected) +{ + if (read_char() != expected) + throw ReadError(string("Expected '") + expected + "'"); +} + +void Reader::read_node(bool is_root) +{ + read_expected(';'); + if (! m_read_only_main_variation || m_is_in_main_variation) + on_begin_node(is_root); + while (true) + { + consume_whitespace(); + char c = peek(); + if (c == '(' || c == ')' || c == ';') + break; + read_property(); + } + if (! m_read_only_main_variation || m_is_in_main_variation) + on_end_node(); +} + +void Reader::read_property() +{ + if (m_read_only_main_variation && ! m_is_in_main_variation) + { + while (peek() != '[') + read_char(); + while (peek() == '[') + { + consume_char('['); + bool escape = false; + while (peek() != ']' || escape) + { + char c = read_char(); + if (c == '\\' && ! escape) + { + escape = true; + continue; + } + escape = false; + } + consume_char(']'); + consume_whitespace(); + } + } + else + { + m_id.clear(); + while (peek() != '[') + m_id += read_char(); + m_values.clear(); + while (peek() == '[') + { + consume_char('['); + m_value.clear(); + bool escape = false; + while (peek() != ']' || escape) + { + char c = read_char(); + if (c == '\\' && ! escape) + { + escape = true; + continue; + } + escape = false; + m_value += c; + } + consume_char(']'); + consume_whitespace(); + m_values.push_back(m_value); + } + on_property(m_id, m_values); + } +} + +void Reader::read_tree(bool is_root) +{ + read_expected('('); + on_begin_tree(is_root); + bool was_root = is_root; + while (true) + { + consume_whitespace(); + char c = peek(); + if (c == ')') + break; + else if (c == ';') + { + read_node(is_root); + is_root = false; + } + else if (c == '(') + read_tree(false); + else + throw ReadError("Extra text before node"); + } + read_expected(')'); + m_is_in_main_variation = false; + on_end_tree(was_root); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf diff --git a/src/libboardgame_sgf/Reader.h b/src/libboardgame_sgf/Reader.h new file mode 100644 index 0000000..34d005d --- /dev/null +++ b/src/libboardgame_sgf/Reader.h @@ -0,0 +1,110 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/Reader.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_SGF_READER_H +#define LIBBOARDGAME_SGF_READER_H + +#include +#include +#include +#include + +namespace libboardgame_sgf { + +using namespace std; + +//----------------------------------------------------------------------------- + +class Reader +{ +public: + class ReadError + : public runtime_error + { + using runtime_error::runtime_error; + }; + + Reader(); + + virtual ~Reader(); + + virtual void on_begin_tree(bool is_root); + + virtual void on_end_tree(bool is_root); + + virtual void on_begin_node(bool is_root); + + virtual void on_end_node(); + + virtual void on_property(const string& id, const vector& values); + + /** Read only the main variation. + Reduces CPU time and memory if only the main variation is needed. */ + void set_read_only_main_variation(bool enable); + + /** Read a game tree from the file. + @param in + @param check_single_tree Throw an error if non-whitespace characters + follow after the tree before the end of the stream. This is mainly + useful to ensure that the input is not a SGF file with multiple game + trees if the caller does not want to handle this case. If + check_single_tree is false, you can call read() multiple times to read + all game trees. + @param[out] more_game_trees_left set to true if check_single_tree is + false and there are more game trees to read. + @throws ReadError */ + void read(istream& in, bool check_single_tree = true, + bool* more_game_trees_left = nullptr); + + /** See read(istream&,bool) */ + void read(const string& file); + +private: + bool m_read_only_main_variation = false; + + bool m_is_in_main_variation; + + istream* m_in; + + /** Local variable in read_property(). + Reused for efficiency. */ + string m_id; + + /** Local variable in read_property(). + Reused for efficiency. */ + string m_value; + + /** Local variable in read_property(). + Reused for efficiency. */ + vector m_values; + + void consume_char(char expected); + + void consume_whitespace(); + + char peek(); + + char read_char(); + + void read_expected(char expected); + + void read_node(bool is_root); + + void read_property(); + + void read_tree(bool is_root); +}; + +inline void Reader::set_read_only_main_variation(bool enable) +{ + m_read_only_main_variation = enable; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf + +#endif // LIBBOARDGAME_SGF_READER_H diff --git a/src/libboardgame_sgf/SgfNode.cpp b/src/libboardgame_sgf/SgfNode.cpp new file mode 100644 index 0000000..5d0e148 --- /dev/null +++ b/src/libboardgame_sgf/SgfNode.cpp @@ -0,0 +1,298 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/SgfNode.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "SgfNode.h" + +#include +#include "MissingProperty.h" +#include "libboardgame_util/Assert.h" + +namespace libboardgame_sgf { + +//----------------------------------------------------------------------------- + +Property::~Property() = default; + +//----------------------------------------------------------------------------- + +SgfNode::SgfNode() = default; + +SgfNode::~SgfNode() = default; + +void SgfNode::append(unique_ptr node) +{ + node->m_parent = this; + if (! m_first_child) + m_first_child = move(node); + else + get_last_child()->m_sibling = move(node); +} + +SgfNode& SgfNode::create_new_child() +{ + unique_ptr node(new SgfNode); + node->m_parent = this; + SgfNode& result = *(node.get()); + auto last_child = get_last_child(); + if (! last_child) + m_first_child = move(node); + else + last_child->m_sibling = move(node); + return result; +} + +void SgfNode::delete_variations() +{ + if (m_first_child) + m_first_child->m_sibling.reset(nullptr); +} + +forward_list::const_iterator SgfNode::find_property( + const string& id) const +{ + return find_if(m_properties.begin(), m_properties.end(), + [&](const Property& p) { return p.id == id; }); +} + +const vector SgfNode::get_multi_property(const string& id) const +{ + auto property = find_property(id); + if (property == m_properties.end()) + return vector(); + else + return property->values; +} + +bool SgfNode::has_property(const string& id) const +{ + return find_property(id) != m_properties.end(); +} + +const SgfNode& SgfNode::get_child(unsigned i) const +{ + LIBBOARDGAME_ASSERT(i < get_nu_children()); + auto child = m_first_child.get(); + while (i > 0) + { + child = child->m_sibling.get(); + --i; + } + return *child; +} + +unsigned SgfNode::get_child_index(const SgfNode& child) const +{ + auto current = m_first_child.get(); + unsigned i = 0; + while (true) + { + if (current == &child) + return i; + current = current->m_sibling.get(); + LIBBOARDGAME_ASSERT(current); + ++i; + } +} + +SgfNode* SgfNode::get_last_child() const +{ + auto node = m_first_child.get(); + if (! node) + return nullptr; + while (node->m_sibling) + node = node->m_sibling.get(); + return node; +} + +unsigned SgfNode::get_nu_children() const +{ + unsigned n = 0; + auto child = m_first_child.get(); + while (child) + { + ++n; + child = child->m_sibling.get(); + } + return n; +} + +const SgfNode* SgfNode::get_previous_sibling() const +{ + if (! m_parent) + return nullptr; + auto child = &m_parent->get_first_child(); + if (child == this) + return nullptr; + do + { + if (child->get_sibling() == this) + return child; + child = child->get_sibling(); + } + while (child); + LIBBOARDGAME_ASSERT(false); + return nullptr; +} + +const string& SgfNode::get_property(const string& id) const +{ + auto property = find_property(id); + if (property == m_properties.end()) + throw MissingProperty(id); + return property->values[0]; +} + +const string& SgfNode::get_property(const string& id, + const string& default_value) const +{ + auto property = find_property(id); + if (property == m_properties.end()) + return default_value; + else + return property->values[0]; +} + +void SgfNode::make_first_child() +{ + LIBBOARDGAME_ASSERT(has_parent()); + auto current_child = m_parent->m_first_child.get(); + if (current_child == this) + return; + while (true) + { + auto sibling = current_child->m_sibling.get(); + if (sibling == this) + { + unique_ptr tmp = move(m_parent->m_first_child); + m_parent->m_first_child = move(current_child->m_sibling); + current_child->m_sibling = move(m_sibling); + m_sibling = move(tmp); + return; + } + current_child = sibling; + } +} + +bool SgfNode::move_property_to_front(const string& id) +{ + auto i = m_properties.begin(); + forward_list::const_iterator previous = m_properties.end(); + for ( ; i != m_properties.end(); ++i) + if (i->id == id) + break; + else + previous = i; + if (i == m_properties.begin() || i == m_properties.end()) + return false; + auto property = *i; + m_properties.erase_after(previous); + m_properties.push_front(property); + return true; +} + +void SgfNode::move_down() +{ + LIBBOARDGAME_ASSERT(has_parent()); + auto current = m_parent->m_first_child.get(); + if (current == this) + { + unique_ptr tmp = move(m_parent->m_first_child); + m_parent->m_first_child = move(m_sibling); + m_sibling = move(m_parent->m_first_child->m_sibling); + m_parent->m_first_child->m_sibling = move(tmp); + return; + } + while (true) + { + auto sibling = current->m_sibling.get(); + if (sibling == this) + { + if (! m_sibling) + return; + unique_ptr tmp = move(current->m_sibling); + current->m_sibling = move(m_sibling); + m_sibling = move(current->m_sibling->m_sibling); + current->m_sibling->m_sibling = move(tmp); + return; + } + current = sibling; + } +} + +void SgfNode::move_up() +{ + LIBBOARDGAME_ASSERT(has_parent()); + auto current = m_parent->m_first_child.get(); + if (current == this) + return; + SgfNode* prev = nullptr; + while (true) + { + auto sibling = current->m_sibling.get(); + if (sibling == this) + { + if (! prev) + { + make_first_child(); + return; + } + unique_ptr tmp = move(prev->m_sibling); + prev->m_sibling = move(current->m_sibling); + current->m_sibling = move(m_sibling); + m_sibling = move(tmp); + return; + } + prev = current; + current = sibling; + } +} + +bool SgfNode::remove_property(const string& id) +{ + forward_list::const_iterator previous = m_properties.end(); + for (auto i = m_properties.begin() ; i != m_properties.end(); ++i) + if (i->id == id) + { + if (previous == m_properties.end()) + m_properties.pop_front(); + else + m_properties.erase_after(previous); + return true; + } + else + previous = i; + return false; +} + +unique_ptr SgfNode::remove_child(SgfNode& child) +{ + auto node = &m_first_child; + unique_ptr* previous = nullptr; + while (true) + { + if (node->get() == &child) + { + unique_ptr result = move(*node); + if (! previous) + m_first_child = move(child.m_sibling); + else + (*previous)->m_sibling = move(child.m_sibling); + result->m_parent = nullptr; + return result; + } + previous = node; + node = &(*node)->m_sibling; + LIBBOARDGAME_ASSERT(node); + } +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf diff --git a/src/libboardgame_sgf/SgfNode.h b/src/libboardgame_sgf/SgfNode.h new file mode 100644 index 0000000..b7f0e60 --- /dev/null +++ b/src/libboardgame_sgf/SgfNode.h @@ -0,0 +1,393 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/SgfNode.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_SGF_SGF_NODE_H +#define LIBBOARDGAME_SGF_SGF_NODE_H + +#include +#include +#include +#include +#include "InvalidPropertyValue.h" +#include "libboardgame_util/Assert.h" +#include "libboardgame_util/StringUtil.h" + +namespace libboardgame_sgf { + +using namespace std; +using libboardgame_util::from_string; +using libboardgame_util::to_string; + +//----------------------------------------------------------------------------- + +struct Property +{ + string id; + + vector values; + + unique_ptr next; + + Property(const Property& p) + : id(p.id), + values(p.values) + { } + + Property(const string& id, const vector& values) + : id(id), + values(values) + { + LIBBOARDGAME_ASSERT(! id.empty()); + LIBBOARDGAME_ASSERT(! values.empty()); + } + + ~Property(); +}; + +//----------------------------------------------------------------------------- + +class SgfNode +{ +public: + /** Iterates over siblings. */ + class Iterator + { + public: + explicit Iterator(const SgfNode* node) + { + m_node = node; + } + + bool operator==(Iterator it) const + { + return m_node == it.m_node; + } + + bool operator!=(Iterator it) const + { + return m_node != it.m_node; + } + + Iterator& operator++() + { + m_node = m_node->get_sibling(); + return *this; + } + + const SgfNode& operator*() const + { + return *m_node; + } + + const SgfNode* operator->() const + { + return m_node; + } + + bool is_null() const + { + return m_node == nullptr; + } + + private: + const SgfNode* m_node; + }; + + /** Range for iterating over the children of a node. */ + class Children + { + public: + explicit Children(const SgfNode& node) + : m_begin(node.get_first_child_or_null()) + { } + + Iterator begin() const { return m_begin; } + + Iterator end() const { return Iterator(nullptr); } + + bool empty() const { return m_begin.is_null(); } + + private: + Iterator m_begin; + }; + + SgfNode(); + + ~SgfNode(); + + /** Append a new child. */ + void append(unique_ptr node); + + bool has_property(const string& id) const; + + /** Get a property. + @pre has_property(id) */ + const string& get_property(const string& id) const; + + const string& get_property(const string& id, + const string& default_value) const; + + const vector get_multi_property(const string& id) const; + + /** Get property parsed as a type. + @pre has_property(id) + @throws InvalidPropertyValue, MissingProperty */ + template + T parse_property(const string& id) const; + + /** Get property parsed as a type with default value. + @throws InvalidPropertyValue */ + template + T parse_property(const string& id, const T& default_value) const; + + /** @return true, if property was added or changed. */ + template + bool set_property(const string& id, const T& value); + + /** @return true, if property was added or changed. */ + bool set_property(const string& id, const char* value); + + /** @return true, if property was added or changed. */ + template + bool set_property(const string& id, const vector& values); + + /** @return true, if node contained the property. */ + bool remove_property(const string& id); + + /** @return true, if the property was found and not already at the + front. */ + bool move_property_to_front(const string& id); + + const forward_list& get_properties() const + { + return m_properties; + } + + Children get_children() const + { + return Children(*this); + } + + SgfNode* get_sibling(); + + SgfNode& get_first_child(); + + const SgfNode& get_first_child() const; + + SgfNode* get_first_child_or_null(); + + const SgfNode* get_first_child_or_null() const; + + const SgfNode* get_sibling() const; + + const SgfNode* get_previous_sibling() const; + + bool has_children() const; + + bool has_single_child() const; + + unsigned get_nu_children() const; + + /** @pre i < get_nu_children() */ + const SgfNode& get_child(unsigned i) const; + + unsigned get_child_index(const SgfNode& child) const; + + /** Get single child. + @pre has_single_child() */ + const SgfNode& get_child() const; + + bool has_parent() const; + + /** Get parent node. + @pre has_parent() */ + const SgfNode& get_parent() const; + + /** Get parent node or null if node has no parent. */ + const SgfNode* get_parent_or_null() const; + + SgfNode& get_parent(); + + SgfNode& create_new_child(); + + /** Remove a child. + @return The removed child node. */ + unique_ptr remove_child(SgfNode& child); + + /** Remove all children. + @return A pointer to the first child (which also owns its siblings), + which can be used to append the children to a different node. */ + unique_ptr remove_children(); + + /** @pre has_parent() */ + void make_first_child(); + + /** Switch place with previous sibling. + If the node is already the first child, nothing happens. + @pre has_parent() */ + void move_up(); + + /** Switch place with sibling. + If the node is the last sibling, nothing happens. + @pre has_parent() */ + void move_down(); + + /** Delete all siblings of the first child. */ + void delete_variations(); + +private: + SgfNode* m_parent = nullptr; + + unique_ptr m_first_child; + + unique_ptr m_sibling; + + /** The properties. + Often a node has only one property (the move), so it saves memory + to use a forward_list instead of a vector. */ + forward_list m_properties; + + forward_list::const_iterator find_property( + const string& id) const; + + SgfNode* get_last_child() const; +}; + +inline const SgfNode& SgfNode::get_child() const +{ + LIBBOARDGAME_ASSERT(has_single_child()); + return *m_first_child; +} + +inline const SgfNode& SgfNode::get_parent() const +{ + LIBBOARDGAME_ASSERT(has_parent()); + return *m_parent; +} + +inline SgfNode& SgfNode::get_parent() +{ + LIBBOARDGAME_ASSERT(has_parent()); + return *m_parent; +} + +inline const SgfNode* SgfNode::get_parent_or_null() const +{ + return m_parent; +} + +inline SgfNode& SgfNode::get_first_child() +{ + LIBBOARDGAME_ASSERT(has_children()); + return *m_first_child.get(); +} + +inline const SgfNode& SgfNode::get_first_child() const +{ + LIBBOARDGAME_ASSERT(has_children()); + return *(m_first_child.get()); +} + +inline SgfNode* SgfNode::get_first_child_or_null() +{ + return m_first_child.get(); +} + +inline const SgfNode* SgfNode::get_first_child_or_null() const +{ + return m_first_child.get(); +} + +inline SgfNode* SgfNode::get_sibling() +{ + return m_sibling.get(); +} + +inline const SgfNode* SgfNode::get_sibling() const +{ + return m_sibling.get(); +} + +inline bool SgfNode::has_children() const +{ + return static_cast(m_first_child); +} + +inline bool SgfNode::has_parent() const +{ + return m_parent != nullptr; +} + +inline bool SgfNode::has_single_child() const +{ + return m_first_child && ! m_first_child->m_sibling; +} + +template +T SgfNode::parse_property(const string& id) const +{ + string value = get_property(id); + T result; + if (! from_string(value, result)) + throw InvalidPropertyValue(id, value); + return result; +} + +template +T SgfNode::parse_property(const string& id, const T& default_value) const +{ + if (! has_property(id)) + return default_value; + return parse_property(id); +} + +inline unique_ptr SgfNode::remove_children() +{ + if (m_first_child) + m_first_child->m_parent = nullptr; + return move(m_first_child); +} + +template +bool SgfNode::set_property(const string& id, const T& value) +{ + vector values(1, value); + return set_property(id, values); +} + +inline bool SgfNode::set_property(const string& id, const char* value) +{ + return set_property(id, value); +} + +template +bool SgfNode::set_property(const string& id, const vector& values) +{ + vector values_to_string; + for (const T& v : values) + values_to_string.push_back(to_string(v)); + forward_list::const_iterator last = m_properties.end(); + for (auto i = m_properties.begin(); i != m_properties.end(); ++i) + if (i->id == id) + { + bool was_changed = (i->values != values_to_string); + i->values = values_to_string; + return was_changed; + } + else + last = i; + if (last == m_properties.end()) + m_properties.emplace_front(id, values_to_string); + else + m_properties.emplace_after(last, id, values_to_string); + return true; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf + +#endif // LIBBOARDGAME_SGF_SGF_NODE_H diff --git a/src/libboardgame_sgf/SgfTree.cpp b/src/libboardgame_sgf/SgfTree.cpp new file mode 100644 index 0000000..f2c1886 --- /dev/null +++ b/src/libboardgame_sgf/SgfTree.cpp @@ -0,0 +1,265 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/SgfTree.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "SgfTree.h" + +#include +#include +#include +#include "libboardgame_sgf/SgfUtil.h" + +namespace libboardgame_sgf { + +using libboardgame_sgf::util::find_root; +using libboardgame_util::trim; + +//----------------------------------------------------------------------------- + +SgfTree::SgfTree() +{ + init(); +} + +SgfTree::~SgfTree() +{ +} + +bool SgfTree::contains(const SgfNode& node) const +{ + return &find_root(node) == &get_root(); +} + +const SgfNode& SgfTree::create_new_child(const SgfNode& node) +{ + m_modified = true; + return non_const(node).create_new_child(); +} + +void SgfTree::delete_all_variations() +{ + if (has_variations()) + m_modified = true; + auto node = &get_root(); + while (node) + { + non_const(*node).delete_variations(); + node = node->get_first_child_or_null(); + } +} + +double SgfTree::get_bad_move(const SgfNode& node) +{ + return node.parse_property("BM", 0); +} + +string SgfTree::get_comment(const SgfNode& node) const +{ + return node.get_property("C", ""); +} + +string SgfTree::get_date_today() +{ + time_t t = time(nullptr); + auto tmp = localtime(&t); + if (! tmp) + return "?"; + char date[128]; + strftime(date, sizeof(date), "%Y-%m-%d", tmp); + return date; +} + +double SgfTree::get_good_move(const SgfNode& node) +{ + return node.parse_property("TE", 0); +} + +unique_ptr SgfTree::get_tree_transfer_ownership() +{ + return move(m_root); +} + +bool SgfTree::has_variations() const +{ + auto node = m_root.get(); + while (node) + { + if (node->get_sibling()) + return true; + node = node->get_first_child_or_null(); + } + return false; +} + +void SgfTree::init() +{ + unique_ptr root(new SgfNode); + m_root = move(root); + m_modified = false; +} + +void SgfTree::init(unique_ptr& root) +{ + m_root = move(root); + m_modified = false; +} + +bool SgfTree::is_doubtful_move(const SgfNode& node) +{ + return node.has_property("DO"); +} + +bool SgfTree::is_interesting_move(const SgfNode& node) +{ + return node.has_property("IT"); +} + +void SgfTree::make_first_child(const SgfNode& node) +{ + auto parent = node.get_parent_or_null(); + if (parent && &parent->get_first_child() != &node) + { + non_const(node).make_first_child(); + m_modified = true; + } +} + +void SgfTree::make_main_variation(const SgfNode& node) +{ + auto current = &non_const(node); + while (current->has_parent()) + { + make_first_child(*current); + current = ¤t->get_parent(); + } +} + +void SgfTree::make_root(const SgfNode& node) +{ + if (&node == &get_root()) + return; + LIBBOARDGAME_ASSERT(contains(node)); + auto& parent = node.get_parent(); + unique_ptr new_root = non_const(parent).remove_child(non_const(node)); + m_root = move(new_root); + m_modified = true; +} + +void SgfTree::move_property_to_front(const SgfNode& node, const string& id) +{ + if (non_const(node).move_property_to_front(id)) + m_modified = true; +} + +void SgfTree::move_down(const SgfNode& node) +{ + if (node.get_sibling()) + { + non_const(node).move_down(); + m_modified = true; + } +} + +void SgfTree::move_up(const SgfNode& node) +{ + auto parent = node.get_parent_or_null(); + if (parent && &parent->get_first_child() != &node) + { + non_const(node).move_up(); + m_modified = true; + } +} + +void SgfTree::remove_move_annotation(const SgfNode& node) +{ + remove_property(node, "BM"); + remove_property(node, "DO"); + remove_property(node, "IT"); + remove_property(node, "TE"); +} + +bool SgfTree::remove_property(const SgfNode& node, const string& id) +{ + bool prop_existed = non_const(node).remove_property(id); + if (prop_existed) + m_modified = true; + return prop_existed; +} + +void SgfTree::set_application(const string& name, const string& version) +{ + if (version.empty()) + set_property(get_root(), "AP", name); + else + set_property(get_root(), "AP", name + ":" + version); +} + +void SgfTree::set_property(const SgfNode& node, const string& id, const char* value) +{ + bool was_changed = non_const(node).set_property(id, value); + if (was_changed) + m_modified = true; +} + +void SgfTree::set_property_remove_empty(const SgfNode& node, const string& id, + const string& value) +{ + string trimmed = trim(value); + if (trimmed.empty()) + remove_property(node, id); + else + set_property(node, id, value); +} + +void SgfTree::set_bad_move(const SgfNode& node, double value) +{ + remove_move_annotation(node); + set_property(node, "BM", value); +} + +void SgfTree::set_comment(const SgfNode& node, const string& s) +{ + set_property_remove_empty(node, "C", s); +} + +void SgfTree::set_date_today() +{ + set_date(get_date_today()); +} + +void SgfTree::set_doubtful_move(const SgfNode& node) +{ + remove_move_annotation(node); + set_property(node, "DO", ""); +} + +void SgfTree::set_good_move(const SgfNode& node, double value) +{ + remove_move_annotation(node); + set_property(node, "TE", value); +} + +void SgfTree::set_interesting_move(const SgfNode& node) +{ + remove_move_annotation(node); + set_property(node, "IT", ""); +} + +const SgfNode& SgfTree::truncate(const SgfNode& node) +{ + LIBBOARDGAME_ASSERT(node.has_parent()); + auto& parent = node.get_parent(); + non_const(parent).remove_child(non_const(node)); + m_modified = true; + return parent; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf diff --git a/src/libboardgame_sgf/SgfTree.h b/src/libboardgame_sgf/SgfTree.h new file mode 100644 index 0000000..8b9a4f9 --- /dev/null +++ b/src/libboardgame_sgf/SgfTree.h @@ -0,0 +1,279 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/SgfTree.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_SGF_SGF_TREE_H +#define LIBBOARDGAME_SGF_SGF_TREE_H + +#include "libboardgame_sgf/SgfNode.h" +#include "libboardgame_util/StringUtil.h" + +namespace libboardgame_sgf { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** SGF tree. + Tree structure of the tree can only be manipulated through member functions + to guarantee a consistent tree structure. Therefore the user is given + only const references to nodes and non-const functions of nodes can only + be called through wrapper functions of the tree (in which case the user + passes in a const reference to the node as an identifier for the node). */ +class SgfTree +{ +public: + SgfTree(); + + virtual ~SgfTree(); + + virtual void init(); + + /** Initialize from an existing SGF tree. + @param root The root node of the SGF tree; the ownership is transferred + to this class. */ + virtual void init(unique_ptr& root); + + /** Get the root node and transfer the ownership to the caller. */ + unique_ptr get_tree_transfer_ownership(); + + /** Check if the tree was modified since the construction or the last call + to init() or clear_modified() */ + bool is_modified() const; + + void set_modified(); + + void clear_modified(); + + const SgfNode& get_root() const; + + const SgfNode& create_new_child(const SgfNode& node); + + /** Truncate a node and its subtree from the tree. + Calling this function deletes the node that is to be truncated and its + complete subtree. + @pre node.has_parent() + @param node The node to be truncated. + @return The parent of the truncated node. */ + const SgfNode& truncate(const SgfNode& node); + + /** Delete all variations but the main variation. */ + void delete_all_variations(); + + /** Make a node the first child of its parent. */ + void make_first_child(const SgfNode& node); + + /** Make a node switch place with its previous sibling (if it is not + already the first child). */ + void move_up(const SgfNode& node); + + /** Make a node switch place with its next sibling (if it is not + already the last child). */ + void move_down(const SgfNode& node); + + /** Make a node the root node of the tree. + All nodes that are not the given node or in the subtree below it are + deleted. Note that this operation in general creates a semantically + invalid tree (e.g. missing GM or CA property in the new root). You need + to add those after this function. In general, you will also have to + examine the nodes in the path to the node in the original tree and then + make the tree valid again after calling make_root(). Typically, you + will have to look at the moves played before this node and convert them + into setup properties to add to the new root such that the board + position at this node is the same as originally. */ + void make_root(const SgfNode& node); + + void make_main_variation(const SgfNode& node); + + bool contains(const SgfNode& node) const; + + template + void set_property(const SgfNode& node, const string& id, const T& value); + + void set_property(const SgfNode& node, const string& id, const char* value); + + template + void set_property(const SgfNode& node, const string& id, + const vector& values); + + void set_property_remove_empty(const SgfNode& node, + const string& id, const string& value); + + bool remove_property(const SgfNode& node, const string& id); + + void move_property_to_front(const SgfNode& node, const string& id); + + /** See Node::remove_children() */ + unique_ptr remove_children(const SgfNode& node); + + void append(const SgfNode& node, unique_ptr child); + + /** Get comment. + @return The comment, or an empty string if the node contains no + comment. */ + string get_comment(const SgfNode& node) const; + + void set_comment(const SgfNode& node, const string& s); + + void remove_move_annotation(const SgfNode& node); + + static double get_good_move(const SgfNode& node); + + void set_good_move(const SgfNode& node, double value = 1); + + static double get_bad_move(const SgfNode& node); + + void set_bad_move(const SgfNode& node, double value = 1); + + static bool is_doubtful_move(const SgfNode& node); + + void set_doubtful_move(const SgfNode& node); + + static bool is_interesting_move(const SgfNode& node); + + void set_interesting_move(const SgfNode& node); + + void set_charset(const string& charset); + + void set_application(const string& name, const string& version = ""); + + string get_date() const; + + void set_date(const string& date); + + /** Get today's date in format YYYY-MM-DD as required by DT property. */ + static string get_date_today(); + + void set_date_today(); + + string get_event() const; + + void set_event(const string& event); + + string get_round() const; + + void set_round(const string& date); + + string get_time() const; + + void set_time(const string& time); + + bool has_variations() const; + +private: + bool m_modified; + + unique_ptr m_root; + + SgfNode& non_const(const SgfNode& node); +}; + +inline void SgfTree::append(const SgfNode& node, unique_ptr child) +{ + if (child) + m_modified = true; + non_const(node).append(move(child)); +} + +inline void SgfTree::clear_modified() +{ + m_modified = false; +} + +inline string SgfTree::get_date() const +{ + return m_root->get_property("DT", ""); +} + +inline string SgfTree::get_event() const +{ + return m_root->get_property("EV", ""); +} + +inline bool SgfTree::is_modified() const +{ + return m_modified; +} + +inline string SgfTree::get_round() const +{ + return m_root->get_property("RO", ""); +} + +inline const SgfNode& SgfTree::get_root() const +{ + return *m_root; +} + +inline string SgfTree::get_time() const +{ + return m_root->get_property("TM", ""); +} + +inline SgfNode& SgfTree::non_const(const SgfNode& node) +{ + LIBBOARDGAME_ASSERT(contains(node)); + return const_cast(node); +} + +inline unique_ptr SgfTree::remove_children(const SgfNode& node) +{ + if (node.has_children()) + m_modified = true; + return non_const(node).remove_children(); +} + +inline void SgfTree::set_charset(const string& charset) +{ + set_property_remove_empty(get_root(), "CA", charset); +} + +inline void SgfTree::set_date(const string& date) +{ + set_property_remove_empty(get_root(), "DT", date); +} + +inline void SgfTree::set_event(const string& event) +{ + set_property_remove_empty(get_root(), "EV", event); +} + +inline void SgfTree::set_modified() +{ + m_modified = true; +} + +template +void SgfTree::set_property(const SgfNode& node, const string& id, const T& value) +{ + bool was_changed = non_const(node).set_property(id, value); + if (was_changed) + m_modified = true; +} + +template +void SgfTree::set_property(const SgfNode& node, const string& id, + const vector& values) +{ + bool was_changed = non_const(node).set_property(id, values); + if (was_changed) + m_modified = true; +} + +inline void SgfTree::set_round(const string& round) +{ + set_property_remove_empty(get_root(), "RO", round); +} + +inline void SgfTree::set_time(const string& time) +{ + set_property_remove_empty(get_root(), "TM", time); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf + +#endif // LIBBOARDGAME_SGF_SGF_TREE_H diff --git a/src/libboardgame_sgf/SgfUtil.cpp b/src/libboardgame_sgf/SgfUtil.cpp new file mode 100644 index 0000000..973ef5e --- /dev/null +++ b/src/libboardgame_sgf/SgfUtil.cpp @@ -0,0 +1,221 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/SgfUtil.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "SgfUtil.h" + +#include +#include +#include "InvalidPropertyValue.h" +#include "TreeWriter.h" +#include "libboardgame_util/StringUtil.h" + +namespace libboardgame_sgf { +namespace util { + +using libboardgame_util::get_letter_coord; + +//----------------------------------------------------------------------------- + +const SgfNode& back_to_main_variation(const SgfNode& node) +{ + if (is_main_variation(node)) + return node; + auto current = &node; + while (! is_main_variation(*current)) + current = ¤t->get_parent(); + return current->get_first_child(); +} + +const SgfNode& beginning_of_branch(const SgfNode& node) +{ + auto current = node.get_parent_or_null(); + if (! current) + return node; + while (true) + { + auto parent = current->get_parent_or_null(); + if (! parent || ! parent->has_single_child()) + break; + current = parent; + } + return *current; +} + +const SgfNode* find_next_comment(const SgfNode& node) +{ + auto current = get_next_node(node); + while (current) + { + if (has_comment(*current)) + return current; + current = get_next_node(*current); + } + return nullptr; +} + +const SgfNode& find_root(const SgfNode& node) +{ + auto current = &node; + while (current->has_parent()) + current = ¤t->get_parent(); + return *current; +} + +const SgfNode& get_last_node(const SgfNode& node) +{ + auto n = &node; + while (n->has_children()) + n = &n->get_first_child(); + return *n; +} + +unsigned get_depth(const SgfNode& node) +{ + unsigned depth = 0; + auto current = &node; + while (current->has_parent()) + { + current = ¤t->get_parent(); + ++depth; + } + return depth; +} + +const char* get_move_annotation(const SgfTree& tree, const SgfNode& node) +{ + double goodMove = tree.get_good_move(node); + if (goodMove > 1) + return "!!"; + if (goodMove > 0) + return "!"; + double badMove = tree.get_bad_move(node); + if (badMove > 1) + return "??"; + if (badMove > 0) + return "?"; + if (tree.is_interesting_move(node)) + return "!?"; + if (tree.is_doubtful_move(node)) + return "?!"; + return ""; +} + +const SgfNode* get_next_earlier_variation(const SgfNode& node) +{ + auto child = &node; + auto current = node.get_parent_or_null(); + while (current && ! child->get_sibling()) + { + child = current; + current = current->get_parent_or_null(); + } + if (! current) + return nullptr; + return child->get_sibling(); +} + +const SgfNode* get_next_node(const SgfNode& node) +{ + auto child = node.get_first_child_or_null(); + if (child) + return child; + return get_next_earlier_variation(node); +} + +void get_path_from_root(const SgfNode& node, vector& path) +{ + auto current = &node; + path.assign(1, current); + while(current->has_parent()) + { + current = ¤t->get_parent(); + path.push_back(current); + } + reverse(path.begin(), path.end()); +} + +string get_variation_string(const SgfNode& node) +{ + string result; + auto current = &node; + unsigned depth = get_depth(*current); + while (current->has_parent()) + { + auto& parent = current->get_parent(); + if (parent.get_nu_children() > 1) + { + unsigned index = parent.get_child_index(*current); + if (index > 0) + { + ostringstream s; + s << depth << get_letter_coord(index); + if (! result.empty()) + s << '-' << result; + result = s.str(); + } + } + current = &parent; + --depth; + } + return result; +} + +bool has_comment(const SgfNode& node) +{ + return node.has_property("C"); +} + +bool has_earlier_variation(const SgfNode& node) +{ + auto current = node.get_parent_or_null(); + if (! current) + return false; + while (true) + { + auto parent = current->get_parent_or_null(); + if (! parent) + return false; + if (! parent->has_single_child()) + return true; + current = parent; + } +} + +bool is_empty(const SgfTree& tree) +{ + auto& root = tree.get_root(); + if (root.has_children()) + return false; + for (auto& p : root.get_properties()) + { + auto& id = p.id; + if (id != "GM" && id != "CA" && id != "AP" && id != "DT") + return false; + } + return true; +} + +bool is_main_variation(const SgfNode& node) +{ + auto current = &node; + while (current->has_parent()) + { + auto& parent = current->get_parent(); + if (current != &parent.get_first_child()) + return false; + current = &parent; + } + return true; +} + +//----------------------------------------------------------------------------- + +} // namespace util +} // namespace libboardgame_sgf diff --git a/src/libboardgame_sgf/SgfUtil.h b/src/libboardgame_sgf/SgfUtil.h new file mode 100644 index 0000000..d06c5f8 --- /dev/null +++ b/src/libboardgame_sgf/SgfUtil.h @@ -0,0 +1,77 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/SgfUtil.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_SGF_SGF_UTIL_H +#define LIBBOARDGAME_SGF_SGF_UTIL_H + +#include +#include "SgfTree.h" + +namespace libboardgame_sgf { +namespace util { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Return the last node in the current variation that had a sibling. */ +const SgfNode& beginning_of_branch(const SgfNode& node); + +/** Find next node with a comment in the iteration through complete tree. + @param node The current node in the iteration. + @return The next node in the iteration through the complete tree + after the current node that has a comment. */ +const SgfNode* find_next_comment(const SgfNode& node); + +const SgfNode& find_root(const SgfNode& node); + +/** Get the depth of a node. + The root node has depth 0. */ +unsigned get_depth(const SgfNode& node); + +/** Get list of nodes from root to a target node. + @param node The target node. + @param[out] path The list of nodes. */ +void get_path_from_root(const SgfNode& node, vector& path); + +const SgfNode& get_last_node(const SgfNode& node); + +/** Get a string representation of move annotation properties. */ +const char* get_move_annotation(const SgfTree& tree, const SgfNode& node); + +/** Get next node for iteration through complete tree. */ +const SgfNode* get_next_node(const SgfNode& node); + +/** Return next variation before this node. */ +const SgfNode* get_next_earlier_variation(const SgfNode& node); + +/** Get a text representation of the variation of a certain node. + The variation string is a sequence of X.Y for each branching into a + variation that is not the first child since the root node separated by + commas, with X being the depth of the child node (starting at 0, and + therefore equivalent to the move number if there are no non-root nodes + without moves) and Y being the number of the child (starting at 1). */ +string get_variation_string(const SgfNode& node); + +/** Check if any previous node had a sibling. */ +bool has_earlier_variation(const SgfNode& node); + +bool is_main_variation(const SgfNode& node); + +const SgfNode& back_to_main_variation(const SgfNode& node); + +bool has_comment(const SgfNode& node); + +/** Check if a tree doesn't contain nodes apart from the root node + or properties apart from some trivial properties (GM, CA, AP or DT) */ +bool is_empty(const SgfTree& tree); + +//----------------------------------------------------------------------------- + +} // namespace util +} // namespace libboardgame_sgf + +#endif // LIBBOARDGAME_SGF_SGF_UTIL_H diff --git a/src/libboardgame_sgf/TreeReader.cpp b/src/libboardgame_sgf/TreeReader.cpp new file mode 100644 index 0000000..31553b7 --- /dev/null +++ b/src/libboardgame_sgf/TreeReader.cpp @@ -0,0 +1,65 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/TreeReader.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "TreeReader.h" + +namespace libboardgame_sgf { + +//----------------------------------------------------------------------------- + +TreeReader::TreeReader() = default; + +TreeReader::~TreeReader() = default; + +unique_ptr TreeReader::get_tree_transfer_ownership() +{ + return move(m_root); +} + +void TreeReader::on_begin_tree(bool is_root) +{ + if (! is_root) + m_stack.push(m_current); +} + +void TreeReader::on_end_tree(bool is_root) +{ + if (! is_root) + { + LIBBOARDGAME_ASSERT(! m_stack.empty()); + m_current = m_stack.top(); + m_stack.pop(); + } +} + +void TreeReader::on_begin_node(bool is_root) +{ + if (is_root) + { + m_root.reset(new SgfNode); + m_current = m_root.get(); + } + else + m_current = &m_current->create_new_child(); +} + +void TreeReader::on_end_node() +{ +} + +void TreeReader::on_property(const string& identifier, + const vector& values) +{ + m_current->set_property(identifier, values); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf diff --git a/src/libboardgame_sgf/TreeReader.h b/src/libboardgame_sgf/TreeReader.h new file mode 100644 index 0000000..e79a994 --- /dev/null +++ b/src/libboardgame_sgf/TreeReader.h @@ -0,0 +1,62 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/TreeReader.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_SGF_TREE_READER_H +#define LIBBOARDGAME_SGF_TREE_READER_H + +#include +#include +#include "Reader.h" +#include "SgfNode.h" + +namespace libboardgame_sgf { + +using namespace std; + +//----------------------------------------------------------------------------- + +class TreeReader + : public Reader +{ +public: + TreeReader(); + + ~TreeReader(); + + void on_begin_tree(bool is_root) override; + + void on_end_tree(bool is_root) override; + + void on_begin_node(bool is_root) override; + + void on_end_node() override; + + void on_property(const string& identifier, + const vector& values) override; + + const SgfNode& get_tree() const; + + /** Get the tree and transfer the ownership to the caller. */ + unique_ptr get_tree_transfer_ownership(); + +private: + SgfNode* m_current = nullptr; + + unique_ptr m_root; + + stack m_stack; +}; + +inline const SgfNode& TreeReader::get_tree() const +{ + return *m_root.get(); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf + +#endif // LIBBOARDGAME_SGF_TREE_READER_H diff --git a/src/libboardgame_sgf/TreeWriter.cpp b/src/libboardgame_sgf/TreeWriter.cpp new file mode 100644 index 0000000..48fc589 --- /dev/null +++ b/src/libboardgame_sgf/TreeWriter.cpp @@ -0,0 +1,60 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/TreeWriter.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "TreeWriter.h" + +namespace libboardgame_sgf { + +//----------------------------------------------------------------------------- + +TreeWriter::TreeWriter(ostream& out, const SgfNode& root) + : m_root(root), + m_writer(out) +{ +} + +TreeWriter::~TreeWriter() +{ +} + +void TreeWriter::write() +{ + m_writer.begin_tree(); + write_node(m_root); + m_writer.end_tree(); +} + +void TreeWriter::write_node(const SgfNode& node) +{ + m_writer.begin_node(); + for (auto& i : node.get_properties()) + write_property(i.id, i.values); + m_writer.end_node(); + if (! node.has_children()) + return; + else if (node.has_single_child()) + write_node(node.get_child()); + else + for (auto& i : node.get_children()) + { + m_writer.begin_tree(); + write_node(i); + m_writer.end_tree(); + } +} + +void TreeWriter::write_property(const string& id, const vector& values) +{ + m_writer.write_property(id, values); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf diff --git a/src/libboardgame_sgf/TreeWriter.h b/src/libboardgame_sgf/TreeWriter.h new file mode 100644 index 0000000..3e9af22 --- /dev/null +++ b/src/libboardgame_sgf/TreeWriter.h @@ -0,0 +1,73 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/TreeWriter.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_SGF_TREE_WRITER_H +#define LIBBOARDGAME_SGF_TREE_WRITER_H + +#include "SgfNode.h" +#include "Writer.h" + +namespace libboardgame_sgf { + +//----------------------------------------------------------------------------- + +class TreeWriter +{ +public: + TreeWriter(ostream& out, const SgfNode& root); + + virtual ~TreeWriter(); + + /** Overridable function to write a property. + Can be used in subclasses, for example, to replace or remove obsolete + properties or do other sanitizing. */ + virtual void write_property(const string& id, + const vector& values); + + + /** @name Formatting options. + Should be set before starting to write. */ + /** @{ */ + + void set_one_prop_per_line(bool enable); + + void set_one_prop_value_per_line(bool enable); + + void set_indent(int indent); + + /** @} */ // @name + + + void write(); + +private: + const SgfNode& m_root; + + Writer m_writer; + + void write_node(const SgfNode& node); +}; + +inline void TreeWriter::set_one_prop_per_line(bool enable) +{ + m_writer.set_one_prop_per_line(enable); +} + +inline void TreeWriter::set_one_prop_value_per_line(bool enable) +{ + m_writer.set_one_prop_value_per_line(enable); +} + +inline void TreeWriter::set_indent(int indent) +{ + m_writer.set_indent(indent); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf + +#endif // LIBBOARDGAME_SGF_TREE_WRITER_H diff --git a/src/libboardgame_sgf/Writer.cpp b/src/libboardgame_sgf/Writer.cpp new file mode 100644 index 0000000..0efe0b1 --- /dev/null +++ b/src/libboardgame_sgf/Writer.cpp @@ -0,0 +1,84 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/Writer.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Writer.h" + +#include + +namespace libboardgame_sgf { + +//----------------------------------------------------------------------------- + +Writer::Writer(ostream& out) + : m_out(out) +{ } + +void Writer::begin_node() +{ + m_is_first_prop = true; + write_indent(); + m_out << ';'; +} + +void Writer::begin_tree() +{ + write_indent(); + m_out << '('; + // Don't indent the first level + if (m_level > 0) + m_current_indent += m_indent; + ++m_level; + if (m_indent >= 0) + m_out << '\n'; +} + +void Writer::end_node() +{ + if (! m_one_prop_per_line && m_indent >= 0) + m_out << '\n'; +} + +void Writer::end_tree() +{ + --m_level; + if (m_level > 0) + m_current_indent -= m_indent; + write_indent(); + m_out << ')'; + if (m_indent >= 0) + m_out << '\n'; +} + +string Writer::get_escaped(const string& s) +{ + ostringstream buffer; + for (char c : s) + { + if (c == ']' || c == '\\') + buffer << '\\' << c; + else if (c == '\t' || c == '\f' || c == '\v') + // Replace whitespace as required by the SGF standard. + buffer << ' '; + else + buffer << c; + } + return buffer.str(); +} + +void Writer::write_indent() +{ + if (m_indent >= 0) + for (int i = 0; i < m_current_indent; ++i) + m_out << ' '; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf diff --git a/src/libboardgame_sgf/Writer.h b/src/libboardgame_sgf/Writer.h new file mode 100644 index 0000000..56c98da --- /dev/null +++ b/src/libboardgame_sgf/Writer.h @@ -0,0 +1,139 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/Writer.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_SGF_WRITER_H +#define LIBBOARDGAME_SGF_WRITER_H + +#include +#include +#include +#include "libboardgame_util/StringUtil.h" + +namespace libboardgame_sgf { + +using namespace std; +using libboardgame_util::to_string; + +//----------------------------------------------------------------------------- + +class Writer +{ +public: + explicit Writer(ostream& out); + + /** @name Formatting options. + Should be set before starting to write. */ + /** @{ */ + + void set_one_prop_per_line(bool enable); + + void set_one_prop_value_per_line(bool enable); + + /** @param indent The number of spaces to indent subtrees, -1 means + to not even use newlines. */ + void set_indent(int indent); + + /** @} */ // @name + + + void begin_tree(); + + void end_tree(); + + void begin_node(); + + void end_node(); + + void write_property(const string& id, const char* value); + + template + void write_property(const string& id, const T& value); + + template + void write_property(const string& id, const vector& values); + +private: + ostream& m_out; + + bool m_one_prop_per_line = false; + + bool m_one_prop_value_per_line = false; + + bool m_is_first_prop; + + int m_indent = 0; + + int m_current_indent = 0; + + unsigned m_level = 0; + + + static string get_escaped(const string& s); + + void write_indent(); +}; + +inline void Writer::set_one_prop_per_line(bool enable) +{ + m_one_prop_per_line = enable; +} + +inline void Writer::set_one_prop_value_per_line(bool enable) +{ + m_one_prop_value_per_line = enable; +} + +inline void Writer::set_indent(int indent) +{ + m_indent = indent; +} + +inline void Writer::write_property(const string& id, const char* value) +{ + vector values(1, value); + write_property(id, values); +} + +template +void Writer::write_property(const string& id, const T& value) +{ + vector values(1, value); + write_property(id, values); +} + +template +void Writer::write_property(const string& id, const vector& values) +{ + if (m_one_prop_per_line && ! m_is_first_prop) + { + write_indent(); + m_out << ' '; + } + m_out << id; + bool is_first_value = true; + for (auto& i : values) + { + if (m_one_prop_per_line && m_one_prop_value_per_line + && ! is_first_value && m_indent >= 0) + { + m_out << '\n'; + int indent = static_cast(m_current_indent + 1 + id.size()); + for (int i = 0; i < indent; ++i) + m_out << ' '; + } + m_out << '[' << get_escaped(to_string(i)) << ']'; + is_first_value = false; + } + if (m_one_prop_per_line && m_indent >= 0) + m_out << '\n'; + m_is_first_prop = false; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf + +#endif // LIBBOARDGAME_SGF_WRITER_H diff --git a/src/libboardgame_sys/CMakeLists.txt b/src/libboardgame_sys/CMakeLists.txt new file mode 100644 index 0000000..a8990ce --- /dev/null +++ b/src/libboardgame_sys/CMakeLists.txt @@ -0,0 +1,7 @@ +add_library(boardgame_sys STATIC + Compiler.h + CpuTime.h + CpuTime.cpp + Memory.h + Memory.cpp +) diff --git a/src/libboardgame_sys/Compiler.h b/src/libboardgame_sys/Compiler.h new file mode 100644 index 0000000..70cda1b --- /dev/null +++ b/src/libboardgame_sys/Compiler.h @@ -0,0 +1,66 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sys/Compiler.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_SYS_COMPILER_H +#define LIBBOARDGAME_SYS_COMPILER_H + +#include +#include +#ifdef __GNUC__ +#include +#include +#endif + +namespace libboardgame_sys { + +using namespace std; + +//----------------------------------------------------------------------------- + +#ifdef __GNUC__ +#define LIBBOARDGAME_FORCE_INLINE inline __attribute__((always_inline)) +#elif defined _MSC_VER +#define LIBBOARDGAME_FORCE_INLINE inline __forceinline +#else +#define LIBBOARDGAME_FORCE_INLINE inline +#endif + +#ifdef __GNUC__ +#define LIBBOARDGAME_NOINLINE __attribute__((noinline)) +#elif defined _MSC_VER +#define LIBBOARDGAME_NOINLINE __declspec(noinline) +#else +#define LIBBOARDGAME_NOINLINE +#endif + +#if defined __GNUC__ && ! defined __ICC && ! defined __clang__ +#define LIBBOARDGAME_FLATTEN __attribute__((flatten)) +#else +#define LIBBOARDGAME_FLATTEN +#endif + +template +string get_type_name(const T& t) +{ +#ifdef __GNUC__ + int status; + char* name_ptr = abi::__cxa_demangle(typeid(t).name(), nullptr, nullptr, + &status); + if (status == 0) + { + string result(name_ptr); + free(name_ptr); + return result; + } +#endif + return typeid(t).name(); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sys + +#endif // LIBBOARDGAME_SYS_COMPILER_H diff --git a/src/libboardgame_sys/CpuTime.cpp b/src/libboardgame_sys/CpuTime.cpp new file mode 100644 index 0000000..2d9b0bc --- /dev/null +++ b/src/libboardgame_sys/CpuTime.cpp @@ -0,0 +1,65 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sys/CpuTime.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "CpuTime.h" + +#ifdef _WIN32 +#include +#endif + +#if HAVE_UNISTD_H +#include +#endif + +#if HAVE_SYS_TIMES_H +#include +#endif + +namespace libboardgame_sys { + +//----------------------------------------------------------------------------- + +double cpu_time() +{ +#ifdef _WIN32 + + FILETIME create; + FILETIME exit; + FILETIME sys; + FILETIME user; + if (! GetProcessTimes(GetCurrentProcess(), &create, &exit, &sys, &user)) + return -1; + ULARGE_INTEGER sys_int; + sys_int.LowPart = sys.dwLowDateTime; + sys_int.HighPart = sys.dwHighDateTime; + ULARGE_INTEGER user_int; + user_int.LowPart = user.dwLowDateTime; + user_int.HighPart = user.dwHighDateTime; + return (sys_int.QuadPart + user_int.QuadPart) * 1e-7; + +#elif HAVE_UNISTD_H && HAVE_SYS_TIMES_H + static double ticks_per_second = double(sysconf(_SC_CLK_TCK)); + struct tms buf; + if (times(&buf) == clock_t(-1)) + return -1; + clock_t clock_ticks = + buf.tms_utime + buf.tms_stime + buf.tms_cutime + buf.tms_cstime; + return double(clock_ticks) / ticks_per_second; + +#else + + return -1; + +#endif +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sys diff --git a/src/libboardgame_sys/CpuTime.h b/src/libboardgame_sys/CpuTime.h new file mode 100644 index 0000000..45b0f13 --- /dev/null +++ b/src/libboardgame_sys/CpuTime.h @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sys/CpuTime.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_SYS_CPU_TIME_H +#define LIBBOARDGAME_SYS_CPU_TIME_H + +namespace libboardgame_sys { + +//----------------------------------------------------------------------------- + +/** Return the CPU time of the current process. + @return The CPU time of the current process in seconds or -1, if the + CPU time cannot be determined. */ +double cpu_time(); + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sys + +#endif // LIBBOARDGAME_SYS_CPU_TIME_H diff --git a/src/libboardgame_sys/Memory.cpp b/src/libboardgame_sys/Memory.cpp new file mode 100644 index 0000000..31fe4a7 --- /dev/null +++ b/src/libboardgame_sys/Memory.cpp @@ -0,0 +1,70 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sys/Memory.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Memory.h" + +#ifdef _WIN32 +#include +#include +#else +#include +#endif +// sysctl() is unsupported on Linux with x32 ABI (last checked on Ubuntu 14.10) +#if HAVE_SYS_SYSCTL_H && ! (defined __x86_64__ && defined __ILP32__) +#include +#endif + +namespace libboardgame_sys { + +//----------------------------------------------------------------------------- + +size_t get_memory() +{ +#ifdef _WIN32 + + MEMORYSTATUSEX status; + status.dwLength = sizeof(status); + if (! GlobalMemoryStatusEx(&status)) + return 0; + auto total_virtual = static_cast(status.ullTotalVirtual); + auto total_phys = static_cast(status.ullTotalPhys); + return min(total_virtual, total_phys); + +#elif defined _SC_PHYS_PAGES + + long phys_pages = sysconf(_SC_PHYS_PAGES); + if (phys_pages < 0) + return 0; + long page_size = sysconf(_SC_PAGE_SIZE); + if (page_size < 0) + return 0; + return static_cast(phys_pages) * static_cast(page_size); + +#elif defined HW_PHYSMEM // Mac OS X + + unsigned int phys_mem; + size_t len = sizeof(phys_mem); + int name[2] = { CTL_HW, HW_PHYSMEM }; + if (sysctl(name, 2, &phys_mem, &len, nullptr, 0) != 0 + || len != sizeof(phys_mem)) + return 0; + else + return phys_mem; + +#else + + return 0; + +#endif +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sys diff --git a/src/libboardgame_sys/Memory.h b/src/libboardgame_sys/Memory.h new file mode 100644 index 0000000..769aa1f --- /dev/null +++ b/src/libboardgame_sys/Memory.h @@ -0,0 +1,26 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sys/Memory.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_SYS_MEMORY_H +#define LIBBOARDGAME_SYS_MEMORY_H + +#include + +namespace libboardgame_sys { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Get the physical memory available on the system. + @return The memory in bytes or 0 if the memory could not be determined. */ +size_t get_memory(); + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sys + +#endif // LIBBOARDGAME_SYS_MEMORY_H diff --git a/src/libboardgame_test/CMakeLists.txt b/src/libboardgame_test/CMakeLists.txt new file mode 100644 index 0000000..715549e --- /dev/null +++ b/src/libboardgame_test/CMakeLists.txt @@ -0,0 +1,4 @@ +add_library(boardgame_test STATIC + Test.h + Test.cpp +) diff --git a/src/libboardgame_test/Test.cpp b/src/libboardgame_test/Test.cpp new file mode 100644 index 0000000..a1fafd4 --- /dev/null +++ b/src/libboardgame_test/Test.cpp @@ -0,0 +1,118 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_test/Test.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Test.h" + +#include +#include +#include "libboardgame_util/Assert.h" +#include "libboardgame_util/Log.h" + +namespace libboardgame_test { + +//----------------------------------------------------------------------------- + +namespace { + +map& get_all_tests() +{ + static map all_tests; + return all_tests; +} + +string get_fail_msg(const char* file, int line, const string& s) +{ + ostringstream msg; + msg << file << ":" << line << ": " << s; + return msg.str(); +} + +} // namespace + +//----------------------------------------------------------------------------- + +TestFail::TestFail(const char* file, int line, const string& s) + : logic_error(get_fail_msg(file, line, s)) +{ +} + +//----------------------------------------------------------------------------- + +void add_test(const string& name, TestFunction function) +{ + auto& all_tests = get_all_tests(); + LIBBOARDGAME_ASSERT(all_tests.find(name) == all_tests.end()); + all_tests.insert(make_pair(name, function)); +} + +bool run_all_tests() +{ + unsigned nu_fail = 0; + LIBBOARDGAME_LOG("Running ", get_all_tests().size(), " tests..."); + for (auto& i : get_all_tests()) + { + try + { + (i.second)(); + } + catch (const TestFail& e) + { + LIBBOARDGAME_LOG(e.what()); + ++nu_fail; + } + } + if (nu_fail == 0) + { + LIBBOARDGAME_LOG("OK"); + return true; + } + else + { + LIBBOARDGAME_LOG(nu_fail, " tests failed.\nFAIL"); + return false; + } +} + +bool run_test(const string& name) +{ + for (auto& i : get_all_tests()) + if (i.first == name) + { + LIBBOARDGAME_LOG("Running ", name, "..."); + try + { + (i.second)(); + LIBBOARDGAME_LOG("OK"); + return true; + } + catch (const TestFail& e) + { + LIBBOARDGAME_LOG(e.what(), "\nFAIL"); + return false; + } + } + LIBBOARDGAME_LOG("Test not found: ", name); + return false; +} + +int test_main(int argc, char* argv[]) +{ + if (argc < 2) + return run_all_tests() ? 0 : 1; + int result = 0; + for (int i = 1; i < argc; ++i) + if (! run_test(argv[i])) + result = 1; + return result; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_test diff --git a/src/libboardgame_test/Test.h b/src/libboardgame_test/Test.h new file mode 100644 index 0000000..4289341 --- /dev/null +++ b/src/libboardgame_test/Test.h @@ -0,0 +1,153 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_test/Test.h + Provides functionality similar to Boost.Test. + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_TEST_TEST_H +#define LIBBOARDGAME_TEST_TEST_H + +#include +#include +#include +#include + +namespace libboardgame_test { + +using namespace std; + +//----------------------------------------------------------------------------- + +typedef void (*TestFunction)(); + +//----------------------------------------------------------------------------- + +class TestFail + : public logic_error +{ +public: + TestFail(const char* file, int line, const string& s); +}; + +//----------------------------------------------------------------------------- + +void add_test(const string& name, TestFunction function); + +bool run_all_tests(); + +bool run_test(const string& name); + +/** Main function that runs all tests (if no arguments) or only the tests + given as arguments. */ +int test_main(int argc, char* argv[]); + +//----------------------------------------------------------------------------- + +/** Helper class that automatically adds a test when an instance is + declared. */ +struct TestRegistrar +{ + TestRegistrar(const string& name, TestFunction function) + { + add_test(name, function); + } +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_test + +//----------------------------------------------------------------------------- + +#define LIBBOARDGAME_TEST_CASE(name) \ + void name(); \ + libboardgame_test::TestRegistrar name##_registrar(#name, name); \ + void name() + + +#define LIBBOARDGAME_CHECK(expr) \ + if (! (expr)) \ + throw libboardgame_test::TestFail(__FILE__, __LINE__, "check failed") + +#define LIBBOARDGAME_CHECK_EQUAL(expr1, expr2) \ + { \ + using libboardgame_test::TestFail; \ + auto result1 = (expr1); \ + auto result2 = (expr2); \ + if (result1 != result2) \ + { \ + ostringstream msg; \ + msg << "'" << result1 << " != " << "'" << result2 << "'"; \ + throw TestFail(__FILE__, __LINE__, msg.str()); \ + } \ + } + +#define LIBBOARDGAME_CHECK_THROW(expr, exception) \ + { \ + using libboardgame_test::TestFail; \ + bool was_thrown = false; \ + try \ + { \ + expr; \ + } \ + catch (const exception&) \ + { \ + was_thrown = true; \ + } \ + if (! was_thrown) \ + { \ + ostringstream msg; \ + msg << "Exception '" << #exception << "' was not thrown"; \ + throw TestFail(__FILE__, __LINE__, msg.str()); \ + } \ + } + +#define LIBBOARDGAME_CHECK_NO_THROW(expr) \ + { \ + using libboardgame_test::TestFail; \ + try \ + { \ + expr; \ + } \ + catch (...) \ + { \ + throw TestFail(__FILE__, __LINE__, \ + "Unexcpected exception was thrown"); \ + } \ + } + +/** Compare floating points using a tolerance in percent. */ +#define LIBBOARDGAME_CHECK_CLOSE(expr1, expr2, tolerance) \ + { \ + using libboardgame_test::TestFail; \ + auto result1 = (expr1); \ + auto result2 = (expr2); \ + if (fabs(result1 - result2) > 0.01 * tolerance * result1) \ + { \ + ostringstream msg; \ + msg << "Difference between " << result1 << " and " \ + << result2 << " exceeds " << (0.01 * tolerance) \ + << " percent"; \ + throw TestFail(__FILE__, __LINE__, msg.str()); \ + } \ + } + +/** Compare floating points using an epsilon. */ +#define LIBBOARDGAME_CHECK_CLOSE_EPS(expr1, expr2, epsilon) \ + { \ + using libboardgame_test::TestFail; \ + auto result1 = (expr1); \ + auto result2 = (expr2); \ + if (fabs(result1 - result2) > epsilon) \ + { \ + ostringstream msg; \ + msg << "Difference between " << result1 << " and " \ + << result2 << " exceeds " << epsilon; \ + throw TestFail(__FILE__, __LINE__, msg.str()); \ + } \ + } + +//----------------------------------------------------------------------------- + +#endif // LIBBOARDGAME_TEST_TEST_H diff --git a/src/libboardgame_test_main/CMakeLists.txt b/src/libboardgame_test_main/CMakeLists.txt new file mode 100644 index 0000000..ed1f426 --- /dev/null +++ b/src/libboardgame_test_main/CMakeLists.txt @@ -0,0 +1 @@ +add_library(boardgame_test_main STATIC Main.cpp) diff --git a/src/libboardgame_test_main/Main.cpp b/src/libboardgame_test_main/Main.cpp new file mode 100644 index 0000000..2b9a586 --- /dev/null +++ b/src/libboardgame_test_main/Main.cpp @@ -0,0 +1,16 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_test_main/Main.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "libboardgame_test/Test.h" + +//----------------------------------------------------------------------------- + +int main(int argc, char* argv[]) +{ + return libboardgame_test::test_main(argc, argv); +} + +//---------------------------------------------------------------------------- diff --git a/src/libboardgame_util/Abort.cpp b/src/libboardgame_util/Abort.cpp new file mode 100644 index 0000000..f4e02bb --- /dev/null +++ b/src/libboardgame_util/Abort.cpp @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Abort.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Abort.h" + +//---------------------------------------------------------------------------- + +namespace libboardgame_util { + +using namespace std; + +atomic abort(false); + +//---------------------------------------------------------------------------- + +} // namespace libboardgame_util diff --git a/src/libboardgame_util/Abort.h b/src/libboardgame_util/Abort.h new file mode 100644 index 0000000..c5c3f94 --- /dev/null +++ b/src/libboardgame_util/Abort.h @@ -0,0 +1,40 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Abort.h + Global flag to interrupt move generation or other commands. + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_ABORT_H +#define LIBBOARDGAME_UTIL_ABORT_H + +#include + +namespace libboardgame_util { + +using namespace std; + +//----------------------------------------------------------------------------- + +extern atomic abort; + +inline void clear_abort() +{ + abort.store(false, memory_order_seq_cst); +} + +inline bool get_abort() +{ + return abort.load(memory_order_relaxed); +} + +inline void set_abort() +{ + abort.store(true, memory_order_seq_cst); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_ABORT_H diff --git a/src/libboardgame_util/ArrayList.h b/src/libboardgame_util/ArrayList.h new file mode 100644 index 0000000..d72cb46 --- /dev/null +++ b/src/libboardgame_util/ArrayList.h @@ -0,0 +1,353 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/ArrayList.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_ARRAY_LIST_H +#define LIBBOARDGAME_UTIL_ARRAY_LIST_H + +#include +#include +#include +#include +#include "Assert.h" + +namespace libboardgame_util { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Array-based list with maximum number of elements. + The user is responsible for not inserting more than the maximum number of + elements. The elements must be default-constructible. If the size of the + list shrinks, the destructor of elements will be not be called and the + elements above the current size are still usable with get_unchecked(). + The list contains iterator definitions that are compatible with STL + containers. + @tparam T The type of the elements + @tparam M The maximum number of elements + @tparam I The integer type for the array size */ +template +class ArrayList +{ +public: + typedef I IntType; + + static_assert(numeric_limits::is_integer, ""); + + static const IntType max_size = M; + + typedef typename array::iterator iterator; + + typedef typename array::const_iterator const_iterator; + + typedef T value_type; + + + ArrayList() = default; + + /** Construct list with a single element. */ + explicit ArrayList(const T& t); + + explicit ArrayList(const initializer_list& l); + + /** Assignment operator. + Copies only elements with index below the current size. */ + ArrayList& operator=(const ArrayList& l); + + T& operator[](I i); + + const T& operator[](I i) const; + + /** Get an element whose index may be higher than the current size. */ + T& get_unchecked(I i); + + /** Get an element whose index may be higher than the current size. */ + const T& get_unchecked(I i) const; + + bool operator==(const ArrayList& array_list) const; + + bool operator!=(const ArrayList& array_list) const; + + iterator begin(); + + const_iterator begin() const; + + iterator end(); + + const_iterator end() const; + + T& back(); + + const T& back() const; + + I size() const; + + bool empty() const; + + const T& pop_back(); + + void push_back(const T& t); + + void clear(); + + void assign(const T& t); + + /** Change the size of the list. + Does not call constructors on new elements if the size grows or + destructors of elements if the size shrinks. */ + void resize(I size); + + bool contains(const T& t) const; + + /** Push back element if not already contained in list. + @return @c true if element was not already in list. */ + bool include(const T& t); + + /** Removal of first occurrence of value. + Preserves the order of elements. + @return @c true if value was removed. */ + bool remove(const T& t); + + /** Fast removal of element. + Does not preserve the order of elements. The element will be replaced + with the last element and the list size decremented. */ + void remove_fast(iterator i); + + /** Fast removal of first occurrence of value. + Does not preserve the order of elements. If the value is found, + it will be replaced with the last element and the list size + decremented. + @return @c true if value was removed. */ + bool remove_fast(const T& t); + +private: + array m_a; + + I m_size = 0; +}; + +template +inline ArrayList::ArrayList(const T& t) +{ + assign(t); +} + +template +ArrayList::ArrayList(const initializer_list& l) + : m_size(0) +{ + for (auto& t : l) + push_back(t); +} + +template +auto ArrayList::operator=(const ArrayList& l) -> ArrayList& +{ + m_size = l.size(); + copy(l.begin(), l.end(), begin()); + return *this; +} + +template +inline T& ArrayList::operator[](I i) +{ + LIBBOARDGAME_ASSERT(i < m_size); + return m_a[i]; +} + +template +inline const T& ArrayList::operator[](I i) const +{ + LIBBOARDGAME_ASSERT(i < m_size); + return m_a[i]; +} + +template +bool ArrayList::operator==(const ArrayList& array_list) const +{ + if (m_size != array_list.m_size) + return false; + return equal(begin(), end(), array_list.begin()); +} + +template +bool ArrayList::operator!=(const ArrayList& array_list) const +{ + return ! operator==(array_list); +} + +template +inline void ArrayList::assign(const T& t) +{ + m_size = 1; + m_a[0] = t; +} + +template +inline T& ArrayList::back() +{ + LIBBOARDGAME_ASSERT(m_size > 0); + return m_a[m_size - 1]; +} + +template +inline const T& ArrayList::back() const +{ + LIBBOARDGAME_ASSERT(m_size > 0); + return m_a[m_size - 1]; +} + +template +inline auto ArrayList::begin() -> iterator +{ + return m_a.begin(); +} + +template +inline auto ArrayList::begin() const -> const_iterator +{ + return m_a.begin(); +} + +template +inline void ArrayList::clear() +{ + m_size = 0; +} + +template +bool ArrayList::contains(const T& t) const +{ + return find(begin(), end(), t) != end(); +} + +template +inline bool ArrayList::empty() const +{ + return m_size == 0; +} + +template +inline auto ArrayList::end() -> iterator +{ + return begin() + m_size; +} + +template +inline auto ArrayList::end() const -> const_iterator +{ + return begin() + m_size; +} + +template +inline T& ArrayList::get_unchecked(I i) +{ + LIBBOARDGAME_ASSERT(i < max_size); + return m_a[i]; +} + +template +inline const T& ArrayList::get_unchecked(I i) const +{ + LIBBOARDGAME_ASSERT(i < max_size); + return m_a[i]; +} + +template +bool ArrayList::include(const T& t) +{ + if (contains(t)) + return false; + push_back(t); + return true; +} + +template +inline const T& ArrayList::pop_back() +{ + LIBBOARDGAME_ASSERT(m_size > 0); + return m_a[--m_size]; +} + +template +inline void ArrayList::push_back(const T& t) +{ + LIBBOARDGAME_ASSERT(m_size < max_size); + m_a[m_size++] = t; +} + +template +inline bool ArrayList::remove(const T& t) +{ + auto end = this->end(); + for (auto i = begin(); i != end; ++i) + if (*i == t) + { + --end; + for ( ; i != end; ++i) + *i = *(i + 1); + --m_size; + return true; + } + return false; +} + +template +inline bool ArrayList::remove_fast(const T& t) +{ + auto end = this->end(); + for (auto i = this->begin(); i != end; ++i) + if (*i == t) + { + remove_fast(i); + return true; + } + return false; +} + +template +inline void ArrayList::remove_fast(iterator i) +{ + LIBBOARDGAME_ASSERT(i >= begin()); + LIBBOARDGAME_ASSERT(i < end()); + --m_size; + *i = *(begin() + m_size); +} + +template +inline void ArrayList::resize(I size) +{ + LIBBOARDGAME_ASSERT(size <= max_size); + m_size = size; +} + +template +inline I ArrayList::size() const +{ + return m_size; +} + +//----------------------------------------------------------------------------- + +template +ostream& operator<<(ostream& out, const ArrayList& l) +{ + auto begin = l.begin(); + auto end = l.end(); + if (begin != end) + { + out << *begin; + for (auto i = begin + 1; i != end; ++i) + out << ' ' << *i; + } + return out; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_ARRAY_LIST_H diff --git a/src/libboardgame_util/Assert.cpp b/src/libboardgame_util/Assert.cpp new file mode 100644 index 0000000..91d6617 --- /dev/null +++ b/src/libboardgame_util/Assert.cpp @@ -0,0 +1,74 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Assert.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Assert.h" + +#include + +#if LIBBOARDGAME_DEBUG +#include +#include +#include +#include +#include +#include "Log.h" +#endif + +namespace libboardgame_util { + +using namespace std; + +//----------------------------------------------------------------------------- + +namespace { + +list& get_all_handlers() +{ + static list all_handlers; + return all_handlers; +} + +} // namespace + +//---------------------------------------------------------------------------- + +AssertionHandler::AssertionHandler() +{ + get_all_handlers().push_back(this); +} + +AssertionHandler::~AssertionHandler() +{ + get_all_handlers().remove(this); +} + +//---------------------------------------------------------------------------- + +#if LIBBOARDGAME_DEBUG + +void handle_assertion(const char* expression, const char* file, int line) +{ + static bool is_during_handle_assertion = false; + LIBBOARDGAME_LOG(file, ":", line, ": Assertion '", expression, "' failed"); + flush_log(); + if (! is_during_handle_assertion) + { + is_during_handle_assertion = true; + for_each(get_all_handlers().begin(), get_all_handlers().end(), + mem_fun(&AssertionHandler::run)); + } + abort(); +} + +#endif + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util diff --git a/src/libboardgame_util/Assert.h b/src/libboardgame_util/Assert.h new file mode 100644 index 0000000..cd696fe --- /dev/null +++ b/src/libboardgame_util/Assert.h @@ -0,0 +1,57 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Assert.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_ASSERT_H +#define LIBBOARDGAME_UTIL_ASSERT_H + +namespace libboardgame_util { + +//----------------------------------------------------------------------------- + +class AssertionHandler +{ +public: + /** Construct and register assertion handler. */ + AssertionHandler(); + + /** Destruct and unregister assertion handler. */ + virtual ~AssertionHandler(); + + virtual void run() = 0; +}; + +#if LIBBOARDGAME_DEBUG + +/** Function used by the LIBBOARDGAME_ASSERT macro to run all assertion + handlers. */ +[[noreturn]] void handle_assertion(const char* expression, const char* file, + int line); + +#endif + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +//----------------------------------------------------------------------------- + +/** @def LIBBOARDGAME_ASSERT + Enhanced assert macro. + This macro is similar to the assert macro in the standard library, but it + allows the user to register assertion handlers that are executed before the + program is aborted. Assertions are only enabled if the macro + LIBBOARDGAME_DEBUG is true. */ +#if LIBBOARDGAME_DEBUG +#define LIBBOARDGAME_ASSERT(expr) \ + ((expr) ? (static_cast(0)) \ + : libboardgame_util::handle_assertion(#expr, __FILE__, __LINE__)) +#else +#define LIBBOARDGAME_ASSERT(expr) (static_cast(0)) +#endif + +//----------------------------------------------------------------------------- + +#endif // LIBBOARDGAME_UTIL_ASSERT_H diff --git a/src/libboardgame_util/Barrier.cpp b/src/libboardgame_util/Barrier.cpp new file mode 100644 index 0000000..cd4a76f --- /dev/null +++ b/src/libboardgame_util/Barrier.cpp @@ -0,0 +1,43 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Barrier.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Barrier.h" + +#include "Assert.h" + +namespace libboardgame_util { + +//---------------------------------------------------------------------------- + +Barrier::Barrier(unsigned count) + : m_threshold(count), + m_count(count) +{ + LIBBOARDGAME_ASSERT(count > 0); +} + +void Barrier::wait() +{ + unique_lock lock(m_mutex); + unsigned current = m_current; + if (--m_count == 0) + { + ++m_current; + m_count = m_threshold; + m_condition.notify_all(); + } + else + while (current == m_current) + m_condition.wait(lock); +} + +//---------------------------------------------------------------------------- + +} // namespace libboardgame_util diff --git a/src/libboardgame_util/Barrier.h b/src/libboardgame_util/Barrier.h new file mode 100644 index 0000000..f96cef8 --- /dev/null +++ b/src/libboardgame_util/Barrier.h @@ -0,0 +1,43 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Barrier.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_BARRIER_H +#define LIBBOARDGAME_UTIL_BARRIER_H + +#include +#include + +namespace libboardgame_util { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Similar to boost::barrier, which does not exist in C++11 */ +class Barrier +{ +public: + explicit Barrier(unsigned count); + + void wait(); + +private: + mutex m_mutex; + + condition_variable m_condition; + + unsigned m_threshold; + + unsigned m_count; + + unsigned m_current = 0; +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_BARRIER_H diff --git a/src/libboardgame_util/CMakeLists.txt b/src/libboardgame_util/CMakeLists.txt new file mode 100644 index 0000000..076e97d --- /dev/null +++ b/src/libboardgame_util/CMakeLists.txt @@ -0,0 +1,34 @@ +add_library(boardgame_util STATIC + Abort.h + Abort.cpp + ArrayList.h + Assert.h + Assert.cpp + Barrier.h + Barrier.cpp + CpuTimeSource.h + CpuTimeSource.cpp + FmtSaver.h + IntervalChecker.h + IntervalChecker.cpp + Log.h + Log.cpp + MathUtil.h + Options.h + Options.cpp + RandomGenerator.h + RandomGenerator.cpp + Range.h + Statistics.h + StringUtil.h + StringUtil.cpp + TimeIntervalChecker.h + TimeIntervalChecker.cpp + Timer.h + Timer.cpp + TimeSource.h + TimeSource.cpp + Unused.h + WallTimeSource.h + WallTimeSource.cpp +) diff --git a/src/libboardgame_util/CpuTimeSource.cpp b/src/libboardgame_util/CpuTimeSource.cpp new file mode 100644 index 0000000..e38be00 --- /dev/null +++ b/src/libboardgame_util/CpuTimeSource.cpp @@ -0,0 +1,26 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/CpuTimeSource.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "CpuTimeSource.h" + +#include "libboardgame_sys/CpuTime.h" + +namespace libboardgame_util { + +//----------------------------------------------------------------------------- + +double CpuTimeSource::operator()() +{ + return libboardgame_sys::cpu_time(); +} + +//---------------------------------------------------------------------------- + +} // namespace libboardgame_util diff --git a/src/libboardgame_util/CpuTimeSource.h b/src/libboardgame_util/CpuTimeSource.h new file mode 100644 index 0000000..b4ad8de --- /dev/null +++ b/src/libboardgame_util/CpuTimeSource.h @@ -0,0 +1,28 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/CpuTimeSource.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_CPU_TIME_SOURCE_H +#define LIBBOARDGAME_UTIL_CPU_TIME_SOURCE_H + +#include "TimeSource.h" + +namespace libboardgame_util { + +//----------------------------------------------------------------------------- + +/** CPU time. + @ref libboardgame_doc_threadsafe_after_construction */ +class CpuTimeSource + : public TimeSource +{ +public: + double operator()() override; +}; +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_CPU_TIME_SOURCE_H diff --git a/src/libboardgame_util/FmtSaver.h b/src/libboardgame_util/FmtSaver.h new file mode 100644 index 0000000..a832e0f --- /dev/null +++ b/src/libboardgame_util/FmtSaver.h @@ -0,0 +1,44 @@ +//---------------------------------------------------------------------------- +/** @file libboardgame_util/FmtSaver.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//---------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_FMT_SAVER_H +#define LIBBOARDGAME_UTIL_FMT_SAVER_H + +#include + +namespace libboardgame_util { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Saves the formatting state of a stream and restores it in its + destructor. */ +class FmtSaver +{ +public: + explicit FmtSaver(ostream& out) + : m_out(out) + { + m_dummy.copyfmt(out); + } + + ~FmtSaver() + { + m_out.copyfmt(m_dummy); + } + +private: + ostream& m_out; + + ios m_dummy{nullptr}; +}; + +//---------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_FMT_SAVER_H diff --git a/src/libboardgame_util/IntervalChecker.cpp b/src/libboardgame_util/IntervalChecker.cpp new file mode 100644 index 0000000..520bbdf --- /dev/null +++ b/src/libboardgame_util/IntervalChecker.cpp @@ -0,0 +1,104 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/IntervalChecker.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "IntervalChecker.h" + +#include +#include "Assert.h" +#if LIBBOARDGAME_UTIL_INTERVAL_CHECKER_DEBUG +#include "Log.h" +#endif + +namespace libboardgame_util { + +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_INTERVAL_CHECKER_DEBUG +#define LIBBOARDGAME_UTIL_INTERVAL_CHECKER_DEBUG 0 +#endif + +//----------------------------------------------------------------------------- + +IntervalChecker::IntervalChecker(TimeSource& time_source, double time_interval, + function f) + : m_time_source(time_source), + m_time_interval(time_interval), + m_function(move(f)) +{ +#if LIBBOARDGAME_UTIL_INTERVAL_CHECKER_DEBUG + log(format("IntervalChecker::IntervalChecker: time_interval=%1%") + % time_interval); +#endif + LIBBOARDGAME_ASSERT(time_interval > 0); +} + +bool IntervalChecker::check_expensive() +{ + if (m_result) + return true; + if (m_is_deterministic) + { + m_result = m_function(); + m_count = m_count_interval; + return m_result; + } + double time = m_time_source(); + if (! m_is_first_check) + { + + double diff = time - m_last_time; + double adjust_factor; + if (diff == 0) + adjust_factor = 10; + else + { + adjust_factor = m_time_interval / diff; + if (adjust_factor > 10) + adjust_factor = 10; + else if (adjust_factor < 0.1) + adjust_factor = 0.1; + } + double new_count_interval = adjust_factor * double(m_count_interval); + if (new_count_interval > double(numeric_limits::max())) + m_count_interval = numeric_limits::max(); + else if (new_count_interval < 1) + m_count_interval = 1; + else + m_count_interval = (unsigned)(new_count_interval); + m_result = m_function(); +#if LIBBOARDGAME_UTIL_INTERVAL_CHECKER_DEBUG + log(format("IntervalChecker::check_expensive: " + "diff=%1% adjust_factor=%2% count_interval=%3%") + % diff % adjust_factor % m_count_interval); +#endif + } + else + { +#if LIBBOARDGAME_UTIL_INTERVAL_CHECKER_DEBUG + log("IntervalChecker::check_expensive: is_first_check"); +#endif + m_is_first_check = false; + } + m_last_time = time; + m_count = m_count_interval; + return m_result; +} + +void IntervalChecker::set_deterministic(unsigned interval) +{ + LIBBOARDGAME_ASSERT(interval >= 1); + m_is_deterministic = true; + m_count = interval; + m_count_interval = interval; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util diff --git a/src/libboardgame_util/IntervalChecker.h b/src/libboardgame_util/IntervalChecker.h new file mode 100644 index 0000000..fcef4e1 --- /dev/null +++ b/src/libboardgame_util/IntervalChecker.h @@ -0,0 +1,79 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/IntervalChecker.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_INTERVAL_CHECKER_H +#define LIBBOARDGAME_UTIL_INTERVAL_CHECKER_H + +#include +#include "libboardgame_util/TimeSource.h" + +namespace libboardgame_util { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Reduces regular calls to an expensive function to a given time interval. + The class assumes that its check() function is called in regular time + intervals and forwards only every n'th call to the expensive function with + n being adjusted dynamically to a given time interval. check() returns + true, if the expensive function was called and returned true in the + past. */ +class IntervalChecker +{ +public: + /** Constructor. + @param time_source (@ref libboardgame_doc_storesref) + @param time_interval The time interval in seconds + @param f The expensive function */ + IntervalChecker(TimeSource& time_source, double time_interval, + function f); + + bool operator()(); + + /** Disable the dynamic updating of the interval. + Can be used if the non-reproducability of the time measurement used + for dynamic updating of the check interval is undesirable. + @param interval The fixed interval (number of calls) to use for calling + the expensive function. (Must be greater zero). */ + void set_deterministic(unsigned interval); + +protected: + TimeSource& m_time_source; + +private: + bool m_is_first_check = true; + + bool m_is_deterministic = false; + + bool m_result = false; + + unsigned m_count = 1; + + unsigned m_count_interval = 1; + + double m_time_interval; + + double m_last_time; + + function m_function; + + bool check_expensive(); +}; + +inline bool IntervalChecker::operator()() +{ + if (--m_count == 0) + return check_expensive(); + else + return m_result; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_INTERVAL_CHECKER_H diff --git a/src/libboardgame_util/Log.cpp b/src/libboardgame_util/Log.cpp new file mode 100644 index 0000000..c73a0a7 --- /dev/null +++ b/src/libboardgame_util/Log.cpp @@ -0,0 +1,124 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Log.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#if ! LIBBOARDGAME_DISABLE_LOG + +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Log.h" + +#include + +#if defined ANDROID || defined __ANDROID__ +#include +#endif + +namespace libboardgame_util { + +//----------------------------------------------------------------------------- + +namespace { + +#if defined ANDROID || defined __ANDROID__ + +class AndroidBuf + : public streambuf +{ +public: + AndroidBuf(); + +protected: + int_type overflow(int_type c) override; + + int sync() override; + +private: + static const unsigned buffer_size = 8192; + + char m_buffer[buffer_size]; +}; + +AndroidBuf::AndroidBuf() +{ + setp(m_buffer, m_buffer + buffer_size - 1); +} + +auto AndroidBuf::overflow(int_type c) -> int_type +{ + if (c == traits_type::eof()) + { + *pptr() = traits_type::to_char_type(c); + sbumpc(); + } + return sync() ? traits_type::eof(): traits_type::not_eof(c); +} + +int AndroidBuf::sync() +{ + int n = 0; + if (pbase() != pptr()) + { + __android_log_print(ANDROID_LOG_INFO, "Native", "%s", + string(pbase(), pptr() - pbase()).c_str()); + n = 0; + setp(m_buffer, m_buffer + buffer_size - 1); + } + return n; +} + +AndroidBuf android_buffer; + +#endif // defined(ANDROID) || defined(__ANDROID__) + +} // namespace + +//----------------------------------------------------------------------------- + +ostream* _log_stream = &cerr; + +//----------------------------------------------------------------------------- + +void _log(const string& s) +{ + if (! _log_stream) + return; + if (s.empty()) + *_log_stream << '\n'; + else if (s.back() == '\n') + *_log_stream << s; + else + { + string line = s; + line += '\n'; + *_log_stream << line; + } +} + +void _log_close() +{ +#if defined ANDROID || defined __ANDROID__ + cerr.rdbuf(nullptr); +#endif +} + +void _log_init() +{ +#if defined ANDROID || defined __ANDROID__ + cerr.rdbuf(&android_buffer); +#endif +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +//----------------------------------------------------------------------------- + +#endif // ! LIBBOARDGAME_DISABLE_LOG diff --git a/src/libboardgame_util/Log.h b/src/libboardgame_util/Log.h new file mode 100644 index 0000000..617fd32 --- /dev/null +++ b/src/libboardgame_util/Log.h @@ -0,0 +1,130 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Log.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_LOG_H +#define LIBBOARDGAME_UTIL_LOG_H + +#include +#include + +namespace libboardgame_util { + +using namespace std; + +//----------------------------------------------------------------------------- + +#if ! LIBBOARDGAME_DISABLE_LOG +extern ostream* _log_stream; +#endif + +inline void disable_logging() +{ +#if ! LIBBOARDGAME_DISABLE_LOG + _log_stream = nullptr; +#endif +} + +inline ostream* get_log_stream() +{ +#if ! LIBBOARDGAME_DISABLE_LOG + return _log_stream; +#else + return nullptr; +#endif +} + +inline void flush_log() +{ +#if ! LIBBOARDGAME_DISABLE_LOG + if (_log_stream) + _log_stream->flush(); +#endif +} + +//----------------------------------------------------------------------------- + +#if ! LIBBOARDGAME_DISABLE_LOG + +/** Initializes the logging functionality. + This is necessary to call on some platforms at the start of the program + before any calls to log(). + @see LogInitializer */ +void _log_init(); + +/** Closes the logging functionality. + This is necessary to call on some platforms before the program exits. + @see LogInitializer */ +void _log_close(); + +/** Helper function needed for log(const Ts&...) */ +template +void _log_buffered(ostream& buffer, const T& t) +{ + buffer << t; +} + +/** Helper function needed for log(const Ts&...) */ +template +void _log_buffered(ostream& buffer, const T& first, const Ts&... rest) +{ + buffer << first; + _log_buffered(buffer, rest...); +} + +/** Write a string to the log stream. + Appends a newline if the output has no newline at the end. */ +void _log(const string& s); + +/** Write a number of arguments to the log stream. + Writes to a buffer first so there is only a single write to the log + stream. Appends a newline if the output has no newline at the end. */ +template +void _log(const Ts&... args) +{ + if (! _log_stream) + return; + ostringstream buffer; + _log_buffered(buffer, args...); + _log(buffer.str()); +} + +#endif // ! LIBBOARDGAME_DISABLE_LOG + +//----------------------------------------------------------------------------- + +class LogInitializer +{ +public: + LogInitializer() + { +#if ! LIBBOARDGAME_DISABLE_LOG + _log_init(); +#endif + } + + ~LogInitializer() + { +#if ! LIBBOARDGAME_DISABLE_LOG + _log_close(); +#endif + } +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +//----------------------------------------------------------------------------- + +#if ! LIBBOARDGAME_DISABLE_LOG +#define LIBBOARDGAME_LOG(...) libboardgame_util::_log(__VA_ARGS__) +#else +#define LIBBOARDGAME_LOG(...) (static_cast(0)) +#endif + +//----------------------------------------------------------------------------- + +#endif // LIBBOARDGAME_UTIL_LOG_H diff --git a/src/libboardgame_util/MathUtil.h b/src/libboardgame_util/MathUtil.h new file mode 100644 index 0000000..4342c0a --- /dev/null +++ b/src/libboardgame_util/MathUtil.h @@ -0,0 +1,37 @@ +//---------------------------------------------------------------------------- +/** @file libboardgame_util/MathUtil.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//---------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_MATH_UTIL_H +#define LIBBOARDGAME_UTIL_MATH_UTIL_H + +namespace libboardgame_util { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Fast approximation of exp(x). + The error is less than 15% for abs(x) \< 10 */ +template +inline T fast_exp(T x) +{ + x = static_cast(1) + x / static_cast(256); + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + return x; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_MATH_UTIL_H diff --git a/src/libboardgame_util/Options.cpp b/src/libboardgame_util/Options.cpp new file mode 100644 index 0000000..4f1a3f9 --- /dev/null +++ b/src/libboardgame_util/Options.cpp @@ -0,0 +1,161 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Options.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Options.h" + +namespace libboardgame_util { + +//---------------------------------------------------------------------------- + +Options::Options(int argc, const char** argv, const vector& specs) +{ + for (auto& s : specs) + { + auto pos = s.find("|"); + if (pos == string::npos) + pos = s.find(":"); + if (pos != string::npos) + m_names.insert(s.substr(0, pos)); + else + m_names.insert(s); + } + + bool end_of_options = false; + for (int n = 1; n < argc; ++n) + { + const string arg = argv[n]; + if (! end_of_options && arg.find("-") == 0 && arg != "-") + { + if (arg == "--") + { + end_of_options = true; + continue; + } + string name; + string value; + bool needs_arg = false; + if (arg.find("--") == 0) + { + // Long option + name = arg.substr(2); + auto sz = name.size(); + bool found = false; + for (auto& spec : specs) + if (spec.find(name) == 0 + && (spec.size() == sz || spec[sz] == '|' + || spec[sz] == ':' )) + { + found = true; + needs_arg = (! spec.empty() && spec.back() == ':'); + break; + } + if (! found) + throw OptionError("Unknown option " + arg); + } + else + { + // Short options + for (unsigned i = 1; i < arg.size(); ++i) + { + auto c = arg[i]; + bool found = false; + for (auto& spec : specs) + { + auto pos = spec.find("|" + string(1, c)); + if (pos != string::npos) + { + name = spec.substr(0, pos); + found = true; + if (! spec.empty() && spec.back() == ':') + { + // If not last option, no space was used to + // append the value + if (i != arg.size() - 1) + value = arg.substr(i + 1); + else + needs_arg = true; + } + break; + } + } + if (! found) + throw OptionError("Unknown option -" + string(1, c)); + if (needs_arg || ! value.empty()) + break; + m_map.insert(make_pair(name, "")); + } + } + if (needs_arg) + { + bool value_found = false; + ++n; + if (n < argc) + { + value = argv[n]; + if (value.empty() || value[0] != '-') + value_found = true; + } + if (! value_found) + throw OptionError("Option --" + name + " needs value"); + } + m_map.insert(make_pair(name, value)); + } + else + m_args.push_back(arg); + } +} + +Options::Options(int argc, char** argv, const vector& specs) + : Options(argc, const_cast(argv), specs) +{ +} + +Options::~Options() +{ +} + +void Options::check_name(const string& name) const +{ + if (m_names.count(name) == 0) + throw OptionError("Internal error: invalid option name " + name); +} + +bool Options::contains(const string& name) const +{ + check_name(name); + return m_map.count(name) > 0; +} + +string Options::get(const string& name) const +{ + check_name(name); + auto pos = m_map.find(name); + if (pos == m_map.end()) + throw OptionError("Missing option --" + name); + return pos->second; +} + +string Options::get(const string& name, const string& default_value) const +{ + check_name(name); + auto pos = m_map.find(name); + if (pos == m_map.end()) + return default_value; + return pos->second; +} + +string Options::get(const string& name, const char* default_value) const +{ + return get(name, string(default_value)); +} + +//---------------------------------------------------------------------------- + +} // namespace libboardgame_util diff --git a/src/libboardgame_util/Options.h b/src/libboardgame_util/Options.h new file mode 100644 index 0000000..4f3504e --- /dev/null +++ b/src/libboardgame_util/Options.h @@ -0,0 +1,124 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Options.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_OPTIONS_H +#define LIBBOARDGAME_UTIL_OPTIONS_H + +#include +#include +#include +#include +#include +#include "StringUtil.h" +#include "libboardgame_sys/Compiler.h" + +namespace libboardgame_util { + +using namespace std; +using libboardgame_sys::get_type_name; + +//---------------------------------------------------------------------------- + +class OptionError + : public runtime_error +{ + using runtime_error::runtime_error; +}; + +//---------------------------------------------------------------------------- + +/** Parser for command line options. + The syntax of options is similar to GNU getopt. Options start with "--" + and an option name. Options have optional short (single-character) names + that are used with a single "-" and can be combined if all but the last + option have no value. A single "--" stops option parsing to support + non-option arguments that start with "-". */ +class Options +{ +public: + /** Create options from arguments to main(). + @param argc + @param argv + @param specs A string per option that describes the option. The + description is the long name of the option, followed by and optional + '|' and a character for the short name of the option, followed by an + optional ':' if the option needs a value. + @throws OptionError on error */ + Options(int argc, const char** argv, const vector& specs); + + /** Overloaded version for con-const character strings in argv. + Needed because the portable signature of main is (int, char**). + argv is not modified by this constructor. */ + Options(int argc, char** argv, const vector& specs); + + ~Options(); + + /** Check if an option exists in the command line arguments. + @param name The (long) option name. */ + bool contains(const string& name) const; + + string get(const string& name) const; + + string get(const string& name, const string& default_value) const; + + string get(const string& name, const char* default_value) const; + + /** Get option value. + @param name The (long) option name. + @throws OptionError If option does not exist or has the wrong type. */ + template + T get(const string& name) const; + + /** Get option value or default value. + @param name The (long) option name. + @param default_value A default value. + @return The option value or the default value if the option does not + exist. */ + template + T get(const string& name, const T& default_value) const; + + /** Remaining command line arguments that are not an option or an option + value. */ + const vector& get_args() const; + +private: + set m_names; + + vector m_args; + + map m_map; + + void check_name(const string& name) const; +}; + +template +T Options::get(const string& name) const +{ + T t; + if (! from_string(get(name), t)) + throw OptionError("Option --" + name + " needs type " + + get_type_name(t)); + return t; +} + +template +T Options::get(const string& name, const T& default_value) const +{ + if (! contains(name)) + return default_value; + return get(name); +} + +inline const vector& Options::get_args() const +{ + return m_args; +} + +//---------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_OPTIONS_H diff --git a/src/libboardgame_util/RandomGenerator.cpp b/src/libboardgame_util/RandomGenerator.cpp new file mode 100644 index 0000000..737ca93 --- /dev/null +++ b/src/libboardgame_util/RandomGenerator.cpp @@ -0,0 +1,74 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/RandomGenerator.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "RandomGenerator.h" + +#include + +namespace libboardgame_util { + +//---------------------------------------------------------------------------- + +namespace { + +bool is_seed_set = false; + +RandomGenerator::ResultType the_seed; + +list& get_all_generators() +{ + static list all_generators; + return all_generators; +} + +RandomGenerator::ResultType get_nondet_seed() +{ + random_device generator; + return generator(); +} + +} // namespace + +//----------------------------------------------------------------------------- + +RandomGenerator::RandomGenerator() +{ + set_seed(is_seed_set ? the_seed : get_nondet_seed()); + get_all_generators().push_back(this); +} + +RandomGenerator::~RandomGenerator() +{ + get_all_generators().remove(this); +} + +bool RandomGenerator::has_global_seed() +{ + return is_seed_set; +} + +void RandomGenerator::set_global_seed(ResultType seed) +{ + is_seed_set = true; + the_seed = seed; + for (RandomGenerator* i : get_all_generators()) + i->set_seed(the_seed); +} + +void RandomGenerator::set_global_seed_last() +{ + if (is_seed_set) + for (RandomGenerator* i : get_all_generators()) + i->set_seed(the_seed); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util diff --git a/src/libboardgame_util/RandomGenerator.h b/src/libboardgame_util/RandomGenerator.h new file mode 100644 index 0000000..4f00966 --- /dev/null +++ b/src/libboardgame_util/RandomGenerator.h @@ -0,0 +1,99 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/RandomGenerator.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_RANDOM_GENERATOR_H +#define LIBBOARDGAME_UTIL_RANDOM_GENERATOR_H + +#include + +namespace libboardgame_util { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Fast pseudo-random number generator. + This is a fast and low-quality pseudo-random number generator for tasks + like opening book move selection or even playouts in Monte-Carlo tree + search (does not seem to be sensitive to the quality of the generator). + All instances of this class register themselves automatically at a + global list of random generators, such that the random seed can be + changed at all existing generators with a single function call. + (@ref libboardgame_doc_threadsafe_after_construction) */ +class RandomGenerator +{ +public: + typedef minstd_rand Generator; + + typedef Generator::result_type ResultType; + + + /** Set seed for all currently existing and future generators. + If this function is never called, a non-deterministic seed is used. */ + static void set_global_seed(ResultType seed); + + /** Set seed to last seed for all currently existing and future + generators. + Sets the seed to the last seed that was set with set_seed(). If no seed + was explicitely defined with set_seed(), then this function does + nothing. */ + static void set_global_seed_last(); + + /** Check if a global seed was set. + User code might want to take more measures if a global seed was set to + become fully deterministic (e.g. avoid decisions based on time + measurements). */ + static bool has_global_seed(); + + + /** Constructor. + Constructs the random generator with the global seed, if one was + defined, otherwise with a non-deterministic seed. */ + RandomGenerator(); + + ~RandomGenerator(); + + void set_seed(ResultType seed); + + ResultType generate(); + + /** Generate a float in [a..b]. */ + float generate_float(float a, float b); + + /** Generate a double in [a..b]. */ + double generate_double(double a, double b); + +private: + Generator m_generator; +}; + +inline RandomGenerator::ResultType RandomGenerator::generate() +{ + return m_generator(); +} + +inline double RandomGenerator::generate_double(double a, double b) +{ + uniform_real_distribution distribution(a, b); + return distribution(m_generator); +} + +inline float RandomGenerator::generate_float(float a, float b) +{ + uniform_real_distribution distribution(a, b); + return distribution(m_generator); +} + +inline void RandomGenerator::set_seed(ResultType seed) +{ + m_generator.seed(seed); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_RANDOM_GENERATOR_H diff --git a/src/libboardgame_util/Range.h b/src/libboardgame_util/Range.h new file mode 100644 index 0000000..b206f34 --- /dev/null +++ b/src/libboardgame_util/Range.h @@ -0,0 +1,52 @@ +//---------------------------------------------------------------------------- +/** @file libboardgame_util/Range.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//---------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_RANGE_H +#define LIBBOARDGAME_UTIL_RANGE_H + +#include + +namespace libboardgame_util { + +//----------------------------------------------------------------------------- + +template +class Range +{ +public: + Range(T* begin, T* end) + : m_begin(begin), + m_end(end) + { } + + T* begin() const { return m_begin; } + + T* end() const { return m_end; } + + size_t size() const { return m_end - m_begin; } + + bool contains(T& t) const; + +private: + T* m_begin; + + T* m_end; +}; + +template +bool Range::contains(T& t) const +{ + for (auto& i : *this) + if (i == t) + return true; + return false; +} + +//---------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_RANGE_H diff --git a/src/libboardgame_util/Statistics.h b/src/libboardgame_util/Statistics.h new file mode 100644 index 0000000..02e4b6b --- /dev/null +++ b/src/libboardgame_util/Statistics.h @@ -0,0 +1,457 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Statistics.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_STATISTICS_H +#define LIBBOARDGAME_UTIL_STATISTICS_H + +#include +#include +#include +#include +#include +#include +#include +#include "FmtSaver.h" + +namespace libboardgame_util { + +using namespace std; + +//----------------------------------------------------------------------------- + +template +class StatisticsBase +{ +public: + /** Constructor. + @param init_val The value to return in get_mean() if count is 0. This + value does not affect the mean returned if count is greater 0. */ + explicit StatisticsBase(FLOAT init_val = 0); + + void add(FLOAT val); + + void clear(FLOAT init_val = 0); + + FLOAT get_count() const; + + FLOAT get_mean() const; + + void write(ostream& out, bool fixed = false, + unsigned precision = 6) const; + +private: + FLOAT m_count; + + FLOAT m_mean; +}; + +template +inline StatisticsBase::StatisticsBase(FLOAT init_val) +{ + clear(init_val); +} + +template +void StatisticsBase::add(FLOAT val) +{ + FLOAT count = m_count; + ++count; + val -= m_mean; + m_mean += val / count; + m_count = count; +} + +template +inline void StatisticsBase::clear(FLOAT init_val) +{ + m_count = 0; + m_mean = init_val; +} + +template +inline FLOAT StatisticsBase::get_count() const +{ + return m_count; +} + +template +inline FLOAT StatisticsBase::get_mean() const +{ + return m_mean; +} + +template +void StatisticsBase::write(ostream& out, bool fixed, + unsigned precision) const +{ + FmtSaver saver(out); + if (fixed) + out << std::fixed; + out << setprecision(precision) << m_mean; +} + +//---------------------------------------------------------------------------- + +template +class Statistics +{ +public: + explicit Statistics(FLOAT init_val = 0); + + void add(FLOAT val); + + void clear(FLOAT init_val = 0); + + FLOAT get_mean() const; + + FLOAT get_count() const; + + FLOAT get_deviation() const; + + FLOAT get_error() const; + + FLOAT get_variance() const; + + void write(ostream& out, bool fixed = false, + unsigned precision = 6) const; + +private: + StatisticsBase m_statistics_base; + + FLOAT m_variance; +}; + +template +inline Statistics::Statistics(FLOAT init_val) +{ + clear(init_val); +} + +template +void Statistics::add(FLOAT val) +{ + if (get_count() > 0) + { + FLOAT count_old = get_count(); + FLOAT mean_old = get_mean(); + m_statistics_base.add(val); + FLOAT mean = get_mean(); + FLOAT count = get_count(); + m_variance = (count_old * (m_variance + mean_old * mean_old) + + val * val) / count - mean * mean; + } + else + { + m_statistics_base.add(val); + m_variance = 0; + } +} + +template +inline void Statistics::clear(FLOAT init_val) +{ + m_statistics_base.clear(init_val); + m_variance = 0; +} + +template +inline FLOAT Statistics::get_count() const +{ + return m_statistics_base.get_count(); +} + +template +inline FLOAT Statistics::get_deviation() const +{ + // m_variance can become negative (due to rounding errors?) + return m_variance < 0 ? 0 : sqrt(m_variance); +} + +template +FLOAT Statistics::get_error() const +{ + auto count = get_count(); + return count == 0 ? 0 : get_deviation() / sqrt(count); +} + +template +inline FLOAT Statistics::get_mean() const +{ + return m_statistics_base.get_mean(); +} + +template +inline FLOAT Statistics::get_variance() const +{ + return m_variance; +} + +template +void Statistics::write(ostream& out, bool fixed, + unsigned precision) const +{ + FmtSaver saver(out); + if (fixed) + out << std::fixed; + out << setprecision(precision) << get_mean() << " dev=" + << get_deviation(); +} + +//---------------------------------------------------------------------------- + +template +class StatisticsExt +{ +public: + explicit StatisticsExt(FLOAT init_val = 0); + + void add(FLOAT val); + + void clear(FLOAT init_val = 0); + + FLOAT get_mean() const; + + FLOAT get_error() const; + + FLOAT get_count() const; + + FLOAT get_max() const; + + FLOAT get_min() const; + + FLOAT get_deviation() const; + + FLOAT get_variance() const; + + void write(ostream& out, bool fixed = false, unsigned precision = 6, + bool integer_values = false, bool with_error = false) const; + + string to_string(bool fixed = false, unsigned precision = 6, + bool integer_values = false, + bool with_error = false) const; + +private: + Statistics m_statistics; + + FLOAT m_max; + + FLOAT m_min; +}; + +template +inline StatisticsExt::StatisticsExt(FLOAT init_val) +{ + clear(init_val); +} + +template +void StatisticsExt::add(FLOAT val) +{ + m_statistics.add(val); + if (val > m_max) + m_max = val; + if (val < m_min) + m_min = val; +} + +template +inline void StatisticsExt::clear(FLOAT init_val) +{ + m_statistics.clear(init_val); + m_min = numeric_limits::max(); + m_max = -numeric_limits::max(); +} + +template +inline FLOAT StatisticsExt::get_count() const +{ + return m_statistics.get_count(); +} + +template +inline FLOAT StatisticsExt::get_deviation() const +{ + return m_statistics.get_deviation(); +} + +template +inline FLOAT StatisticsExt::get_error() const +{ + return m_statistics.get_error(); +} + +template +inline FLOAT StatisticsExt::get_max() const +{ + return m_max; +} + +template +inline FLOAT StatisticsExt::get_mean() const +{ + return m_statistics.get_mean(); +} + +template +inline FLOAT StatisticsExt::get_min() const +{ + return m_min; +} + +template +inline FLOAT StatisticsExt::get_variance() const +{ + return m_statistics.get_variance(); +} + +template +string StatisticsExt::to_string(bool fixed, unsigned precision, + bool integer_values, + bool with_error) const +{ + ostringstream s; + write(s, fixed, precision, integer_values, with_error); + return s.str(); +} + +template +void StatisticsExt::write(ostream& out, bool fixed, unsigned precision, + bool integer_values, bool with_error) const +{ + FmtSaver saver(out); + out << setprecision(precision); + if (fixed) + out << std::fixed; + out << get_mean(); + if (with_error) + out << "+-" << get_error(); + out << " dev=" << get_deviation(); + if (integer_values) + out << setprecision(0); + out << " min="; + if (m_min == numeric_limits::max()) + out << "-"; + else + out << m_min; + out << " max="; + if (m_max == -numeric_limits::max()) + out << "-"; + else + out << m_max; +} + +//---------------------------------------------------------------------------- + +/** Like StatisticsBase, but for lock-free multithreading with potentially + lost updates. + Updates and accesses of the moving average and the count are atomic but + not synchronized and use memory_order_relaxed. Therefore, updates can be + lost. Initializing via the constructor, operator= or clear() uses + memory_order_seq_cst */ +template +class StatisticsDirtyLockFree +{ +public: + /** Constructor. + @param init_val See StatisticBase::StatisticBase() */ + explicit StatisticsDirtyLockFree(FLOAT init_val = 0); + + StatisticsDirtyLockFree& operator=(const StatisticsDirtyLockFree& s); + + void add(FLOAT val, FLOAT weight = 1); + + void clear(FLOAT init_val = 0); + + void init(FLOAT mean, FLOAT count); + + FLOAT get_count() const; + + FLOAT get_mean() const; + + void write(ostream& out, bool fixed = false, + unsigned precision = 6) const; + +private: + atomic m_count; + + atomic m_mean; +}; + +template +inline StatisticsDirtyLockFree::StatisticsDirtyLockFree(FLOAT init_val) +{ + clear(init_val); +} + +template +StatisticsDirtyLockFree& +StatisticsDirtyLockFree::operator=(const StatisticsDirtyLockFree& s) +{ + m_count = s.m_count.load(); + m_mean = s.m_mean.load(); + return *this; +} + +template +void StatisticsDirtyLockFree::add(FLOAT val, FLOAT weight) +{ + FLOAT count = m_count.load(memory_order_relaxed); + FLOAT mean = m_mean.load(memory_order_relaxed); + count += weight; + mean += weight * (val - mean) / count; + m_mean.store(mean, memory_order_relaxed); + m_count.store(count, memory_order_relaxed); +} + +template +inline void StatisticsDirtyLockFree::clear(FLOAT init_val) +{ + init(init_val, 0); +} + +template +inline FLOAT StatisticsDirtyLockFree::get_count() const +{ + return m_count.load(memory_order_relaxed); +} + +template +inline FLOAT StatisticsDirtyLockFree::get_mean() const +{ + return m_mean.load(memory_order_relaxed); +} + +template +inline void StatisticsDirtyLockFree::init(FLOAT mean, FLOAT count) +{ + m_count = count; + m_mean = mean; +} + +template +void StatisticsDirtyLockFree::write(ostream& out, bool fixed, + unsigned precision) const +{ + FmtSaver saver(out); + if (fixed) + out << std::fixed; + out << setprecision(precision) << get_mean(); +} + +//---------------------------------------------------------------------------- + +template +inline ostream& operator<<(ostream& out, const StatisticsExt& s) +{ + s.write(out); + return out; +} + +//---------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_STATISTICS_H diff --git a/src/libboardgame_util/StringUtil.cpp b/src/libboardgame_util/StringUtil.cpp new file mode 100644 index 0000000..f95529a --- /dev/null +++ b/src/libboardgame_util/StringUtil.cpp @@ -0,0 +1,105 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/StringUtil.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "StringUtil.h" + +#include +#include + +namespace libboardgame_util { + +//----------------------------------------------------------------------------- + +template<> +bool from_string(const string& s, string& t) +{ + t = s; + return true; +} + +string get_letter_coord(unsigned i) +{ + string result; + while (true) + { + result.insert(0, 1, char('a' + i % 26)); + i /= 26; + if (i == 0) + break; + --i; + } + return result; +} + +vector split(const string& s, char separator) +{ + vector result; + string current; + for (char c : s) + { + if (c == separator) + { + result.push_back(current); + current.clear(); + continue; + } + current.push_back(c); + } + if (! current.empty() || ! result.empty()) + result.push_back(current); + return result; +} + +string time_to_string(double seconds, bool with_seconds_as_double) +{ + int int_seconds = int(seconds + 0.5); + int hours = int_seconds / 3600; + int_seconds -= hours * 3600; + int minutes = int_seconds / 60; + int_seconds -= minutes * 60; + ostringstream s; + s << setfill('0'); + if (hours > 0) + s << hours << ':'; + s << setw(2) << minutes << ':' << setw(2) << int_seconds; + if (with_seconds_as_double) + s << " (" << seconds << ')'; + return s.str(); +} + +string to_lower(string s) +{ + for (auto& c : s) + c = static_cast(tolower(c)); + return s; +} + +string trim(const string& s) +{ + string::size_type begin = 0; + auto end = s.size(); + while (begin != end && isspace(s[begin])) + ++begin; + while (end > begin && isspace(s[end - 1])) + --end; + return s.substr(begin, end - begin); +} + +string trim_right(const string& s) +{ + auto end = s.size(); + while (end > 0 && isspace(s[end - 1])) + --end; + return s.substr(0, end); +} + +//---------------------------------------------------------------------------- + +} // namespace libboardgame_util diff --git a/src/libboardgame_util/StringUtil.h b/src/libboardgame_util/StringUtil.h new file mode 100644 index 0000000..65fb83b --- /dev/null +++ b/src/libboardgame_util/StringUtil.h @@ -0,0 +1,58 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/StringUtil.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_STRING_UTIL_H +#define LIBBOARDGAME_UTIL_STRING_UTIL_H + +#include +#include +#include + +namespace libboardgame_util { + +using namespace std; + +//----------------------------------------------------------------------------- + +template +bool from_string(const string& s, T& t) +{ + istringstream in(s); + in >> t; + return ! in.fail(); +} + +template<> +bool from_string(const string& s, string& t); + +/** Get a letter representing a coordinate. + Returns 'a' to 'z' for i between 0 and 25 and continues with 'aa','ab'... + for coordinates larger than 25. */ +string get_letter_coord(unsigned i); + +vector split(const string& s, char separator); + +string time_to_string(double seconds, bool with_seconds_as_double = false); + +template +string to_string(const T& t) +{ + ostringstream buffer; + buffer << t; + return buffer.str(); +} + +string to_lower(string s); + +string trim(const string& s); + +string trim_right(const string& s); + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_STRING_UTIL_H diff --git a/src/libboardgame_util/TimeIntervalChecker.cpp b/src/libboardgame_util/TimeIntervalChecker.cpp new file mode 100644 index 0000000..0877e6d --- /dev/null +++ b/src/libboardgame_util/TimeIntervalChecker.cpp @@ -0,0 +1,43 @@ +//----------------------------------------------------------------------------- +/** @file TimeIntervalChecker.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "TimeIntervalChecker.h" + +namespace libboardgame_util { + +//----------------------------------------------------------------------------- + +TimeIntervalChecker::TimeIntervalChecker(TimeSource& time_source, + double time_interval, + double max_time) + : IntervalChecker(time_source, time_interval, + bind(&TimeIntervalChecker::check_time, this)), + m_max_time(max_time), + m_start_time(m_time_source()) +{ +} + +TimeIntervalChecker::TimeIntervalChecker(TimeSource& time_source, + double max_time) + : IntervalChecker(time_source, max_time > 1 ? 0.1 : 0.1 * max_time, + bind(&TimeIntervalChecker::check_time, this)), + m_max_time(max_time), + m_start_time(m_time_source()) +{ +} + +bool TimeIntervalChecker::check_time() +{ + return m_time_source() - m_start_time > m_max_time; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util diff --git a/src/libboardgame_util/TimeIntervalChecker.h b/src/libboardgame_util/TimeIntervalChecker.h new file mode 100644 index 0000000..d898b02 --- /dev/null +++ b/src/libboardgame_util/TimeIntervalChecker.h @@ -0,0 +1,41 @@ +//----------------------------------------------------------------------------- +/** @file TimeIntervalChecker.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_TIME_INTERVAL_CHECKER_H +#define LIBBOARDGAME_UTIL_TIME_INTERVAL_CHECKER_H + +#include "IntervalChecker.h" + +namespace libboardgame_util { + +//----------------------------------------------------------------------------- + +/** IntervalChecker that checks if a maximum total time was reached. */ +class TimeIntervalChecker + : public IntervalChecker +{ +public: + TimeIntervalChecker(TimeSource& time_source, double time_interval, + double max_time); + + /** Constructor with automatically set time_interval. + The time interval will be set to 0.1, if max_time > 1, otherwise + to 0.1 * max_time */ + TimeIntervalChecker(TimeSource& time_source, double max_time); + +private: + double m_max_time; + + double m_start_time; + + bool check_time(); +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_TIME_INTERVAL_CHECKER_H diff --git a/src/libboardgame_util/TimeSource.cpp b/src/libboardgame_util/TimeSource.cpp new file mode 100644 index 0000000..43f2a24 --- /dev/null +++ b/src/libboardgame_util/TimeSource.cpp @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/TimeSource.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "TimeSource.h" + +namespace libboardgame_util { + +//----------------------------------------------------------------------------- + +TimeSource::~TimeSource() +{ +} + +//---------------------------------------------------------------------------- + +} // namespace libboardgame_util diff --git a/src/libboardgame_util/TimeSource.h b/src/libboardgame_util/TimeSource.h new file mode 100644 index 0000000..bc040fd --- /dev/null +++ b/src/libboardgame_util/TimeSource.h @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/TimeSource.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_TIME_SOURCE_H +#define LIBBOARDGAME_UTIL_TIME_SOURCE_H + +namespace libboardgame_util { + +//----------------------------------------------------------------------------- + +/** Abstract time source for measuring thinking times for move generation. + Typical implementations are wall time, CPU time or mock time sources + for unit tests. They do not need to provide high resolutions (but should + support at least 100 ms) and should support maximum times of days (or even + months). + @ref libboardgame_doc_threadsafe_after_construction */ +class TimeSource +{ +public: + virtual ~TimeSource(); + + /** Get the current time in seconds. */ + virtual double operator()() = 0; +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_TIME_SOURCE_H diff --git a/src/libboardgame_util/Timer.cpp b/src/libboardgame_util/Timer.cpp new file mode 100644 index 0000000..68a8a3e --- /dev/null +++ b/src/libboardgame_util/Timer.cpp @@ -0,0 +1,43 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Timer.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Timer.h" + +#include "Assert.h" + +namespace libboardgame_util { + +//----------------------------------------------------------------------------- + +Timer::Timer(TimeSource& time_source) + : m_start(time_source()), + m_time_source(&time_source) +{ } + +double Timer::operator()() const +{ + LIBBOARDGAME_ASSERT(m_time_source); + return (*m_time_source)() - m_start; +} + +void Timer::reset() +{ + m_start = (*m_time_source)(); +} + +void Timer::reset(TimeSource& time_source) +{ + m_time_source = &time_source; + reset(); +} + +//---------------------------------------------------------------------------- + +} // namespace libboardgame_util diff --git a/src/libboardgame_util/Timer.h b/src/libboardgame_util/Timer.h new file mode 100644 index 0000000..13a23b9 --- /dev/null +++ b/src/libboardgame_util/Timer.h @@ -0,0 +1,42 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Timer.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_TIMER_H +#define LIBBOARDGAME_UTIL_TIMER_H + +#include "TimeSource.h" + +namespace libboardgame_util { + +class Timer +{ +public: + /** Constructor without time source. + If constructed without time source, the timer cannot be used before + reset(TimeSource&) was called. */ + Timer() = default; + + /** Constructor. + @param time_source (@ref libboardgame_doc_storesref) */ + explicit Timer(TimeSource& time_source); + + double operator()() const; + + void reset(); + + void reset(TimeSource& time_source); + +private: + double m_start; + + TimeSource* m_time_source = nullptr; +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_TIMER_H diff --git a/src/libboardgame_util/Unused.h b/src/libboardgame_util/Unused.h new file mode 100644 index 0000000..43be3cf --- /dev/null +++ b/src/libboardgame_util/Unused.h @@ -0,0 +1,22 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Unused.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_UNUSED_H +#define LIBBOARDGAME_UTIL_UNUSED_H + +//----------------------------------------------------------------------------- + +template static void LIBBOARDGAME_UNUSED(const T&) { } + +#if LIBBOARDGAME_DEBUG +#define LIBBOARDGAME_UNUSED_IF_NOT_DEBUG(x) +#else +#define LIBBOARDGAME_UNUSED_IF_NOT_DEBUG(x) LIBBOARDGAME_UNUSED(x) +#endif + +//----------------------------------------------------------------------------- + +#endif // LIBBOARDGAME_UTIL_UNUSED_H diff --git a/src/libboardgame_util/WallTimeSource.cpp b/src/libboardgame_util/WallTimeSource.cpp new file mode 100644 index 0000000..754e20f --- /dev/null +++ b/src/libboardgame_util/WallTimeSource.cpp @@ -0,0 +1,29 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/WallTimeSource.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "WallTimeSource.h" + +#include + +namespace libboardgame_util { + +using namespace std::chrono; + +//----------------------------------------------------------------------------- + +double WallTimeSource::operator()() +{ + auto t = system_clock::now().time_since_epoch(); + return duration_cast>(t).count(); +} + +//---------------------------------------------------------------------------- + +} // namespace libboardgame_util diff --git a/src/libboardgame_util/WallTimeSource.h b/src/libboardgame_util/WallTimeSource.h new file mode 100644 index 0000000..9c99371 --- /dev/null +++ b/src/libboardgame_util/WallTimeSource.h @@ -0,0 +1,28 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/WallTimeSource.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBBOARDGAME_UTIL_WALL_TIME_SOURCE_H +#define LIBBOARDGAME_UTIL_WALL_TIME_SOURCE_H + +#include "TimeSource.h" + +namespace libboardgame_util { + +//----------------------------------------------------------------------------- + +/** Wall time. + @ref libboardgame_doc_threadsafe_after_construction */ +class WallTimeSource + : public TimeSource +{ +public: + double operator()() override; +}; +//----------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_WALL_TIME_SOURCE_H diff --git a/src/libpentobi_base/Board.cpp b/src/libpentobi_base/Board.cpp new file mode 100644 index 0000000..a06ddcf --- /dev/null +++ b/src/libpentobi_base/Board.cpp @@ -0,0 +1,799 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Board.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Board.h" + +#include +#include "CallistoGeometry.h" +#include "MoveMarker.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +namespace { + +void write_x_coord(ostream& out, unsigned width, unsigned offset) +{ + for (unsigned i = 0; i < offset; ++i) + out << ' '; + char c = 'A'; + for (unsigned x = 0; x < width; ++x, ++c) + { + if (x < 26) + out << ' '; + else + out << 'A'; + if (x == 26) + c = 'A'; + out << c; + } + out << '\n'; +} + +void set_color(ostream& out, const char* esc_sequence) +{ + if (Board::color_output) + out << esc_sequence; +} + +} // namespace + +//----------------------------------------------------------------------------- + +bool Board::color_output = false; + +Board::Board(Variant variant) +{ + m_color_char[Color(0)] = 'X'; + m_color_char[Color(1)] = 'O'; + m_color_char[Color(2)] = '#'; + m_color_char[Color(3)] = '@'; + for_each_color([&](Color c) { + m_state_color[c].forbidden[Point::null()] = false; + }); + init_variant(variant); + init(); +#if LIBBOARDGAME_DEBUG + m_snapshot.moves_size = + numeric_limits::max(); +#endif +} + +void Board::copy_from(const Board& bd) +{ + if (m_variant != bd.m_variant) + init_variant(bd.m_variant); + m_moves = bd.m_moves; + m_setup.to_play = bd.m_setup.to_play; + m_state_base = bd.m_state_base; + for (Color c : get_colors()) + { + m_state_color[c] = bd.m_state_color[c]; + m_setup.placements[c] = bd.m_setup.placements[c]; + m_attach_points[c] = bd.m_attach_points[c]; + } +} + +const Transform* Board::find_transform(Move mv) const +{ + auto& geo = get_geometry(); + PiecePoints points; + for (Point p : get_move_points(mv)) + points.push_back(CoordPoint(geo.get_x(p), geo.get_y(p))); + return get_piece_info(get_move_piece(mv)).find_transform(geo, points); +} + +void Board::gen_moves(Color c, MoveMarker& marker, MoveList& moves) const +{ + moves.clear(); + bool is_callisto = (m_piece_set == PieceSet::callisto); + if (! is_callisto && is_first_piece(c)) + { + for (Point p : get_starting_points(c)) + if (! m_state_color[c].forbidden[p]) + { + auto adj_status = get_adj_status(p, c); + for (Piece piece : m_state_color[c].pieces_left) + gen_moves(c, p, piece, adj_status, marker, moves); + } + return; + } + if (is_callisto && is_piece_left(c, m_one_piece)) + for (auto p : *m_geo) + if (! is_forbidden(p, c) && ! m_is_center_section[p]) + gen_moves(c, p, m_one_piece, get_adj_status(p, c), marker, + moves); + for (Point p : get_attach_points(c)) + if (! m_state_color[c].forbidden[p]) + { + auto adj_status = get_adj_status(p, c); + for (Piece piece : m_state_color[c].pieces_left) + if (! is_callisto || piece != m_one_piece) + gen_moves(c, p, piece, adj_status, marker, moves); + } +} + +void Board::gen_moves(Color c, Point p, Piece piece, unsigned adj_status, + MoveMarker& marker, MoveList& moves) const +{ + for (Move mv : m_bc->get_moves(piece, p, adj_status)) + if (! marker[mv] && ! is_forbidden(c, mv)) + { + moves.push_back(mv); + marker.set(mv); + } +} + +ScoreType Board::get_bonus(Color c) const +{ + if (get_pieces_left(c).size() > 0) + return 0; + auto bonus = m_bonus_all_pieces; + unsigned i = m_moves.size(); + while (i > 0) + { + --i; + if (m_moves[i].color == c) + { + auto piece = get_move_piece(m_moves[i].move); + if (m_score_points[piece] == 1) + bonus += m_bonus_one_piece; + break; + } + } + return bonus; +} + +Color Board::get_effective_to_play() const +{ + return get_effective_to_play(get_to_play()); +} + +Color Board::get_effective_to_play(Color c) const +{ + Color result = c; + do + { + if (has_moves(result)) + return result; + result = get_next(result); + } + while (result != c); + return result; +} + +void Board::get_place(Color c, unsigned& place, bool& is_shared) const +{ + bool break_ties = (m_piece_set == PieceSet::callisto); + array all_scores; + for (Color::IntType i = 0; i < Color::range; ++i) + { + all_scores[i] = get_score(Color(i)); + if (break_ties) + all_scores[i] += i * 0.0001f; + } + auto score = all_scores[c.to_int()]; + sort(all_scores.begin(), all_scores.begin() + m_nu_players, + greater()); + is_shared = false; + bool found = false; + for (unsigned i = 0; i < m_nu_players; ++i) + if (all_scores[i] == score) + { + if (! found) + { + place = i; + found = true; + } + else + is_shared = true; + } +} + +Move Board::get_move_at(Point p) const +{ + auto s = get_point_state(p); + if (s.is_color()) + { + auto c = s.to_color(); + for (Move mv : m_setup.placements[c]) + if (get_move_points(mv).contains(p)) + return mv; + for (ColorMove color_mv : m_moves) + if (color_mv.color == c) + { + Move mv = color_mv.move; + if (get_move_points(mv).contains(p)) + return mv; + } + } + return Move::null(); +} + +bool Board::has_moves(Color c) const +{ + bool is_callisto = (m_piece_set == PieceSet::callisto); + if (is_callisto && is_piece_left(c, m_one_piece)) + for (auto p : *m_geo) + if (! is_forbidden(p, c) && ! m_is_center_section[p]) + return true; + if (! is_callisto && is_first_piece(c)) + { + for (auto p : get_starting_points(c)) + if (has_moves(c, p)) + return true; + return false; + } + for (auto p : get_attach_points(c)) + if (has_moves(c, p)) + return true; + return false; +} + +bool Board::has_moves(Color c, Point p) const +{ + if (is_forbidden(p, c)) + return false; + bool is_callisto = (m_piece_set == PieceSet::callisto); + if (is_callisto && is_piece_left(c, m_one_piece)) + if (m_is_center_section[p]) + return true; + auto adj_status = get_adj_status(p, c); + for (auto piece : m_state_color[c].pieces_left) + { + if (piece == m_one_piece && is_callisto) + continue; + for (auto mv : m_bc->get_moves(piece, p, adj_status)) + if (! is_forbidden(c, mv)) + return true; + } + return false; +} + +bool Board::has_setup() const +{ + for (Color c : get_colors()) + if (! m_setup.placements[c].empty()) + return true; + return false; +} + +void Board::init(Variant variant, const Setup* setup) +{ + if (variant != m_variant) + init_variant(variant); + + // If you make changes here, make sure that you also update copy_from() + + m_state_base.point_state.fill(PointState::empty(), *m_geo); + for (Color c : get_colors()) + { + auto& state = m_state_color[c]; + state.forbidden.fill(false, *m_geo); + state.is_attach_point.fill(false, *m_geo); + state.pieces_left.clear(); + state.nu_onboard_pieces = 0; + state.points = 0; + for (Piece::IntType i = 0; i < get_nu_uniq_pieces(); ++i) + { + Piece piece(i); + state.pieces_left.push_back(piece); + state.nu_left_piece[piece] = + static_cast(get_nu_piece_instances(piece)); + } + m_attach_points[c].clear(); + } + m_state_base.nu_onboard_pieces_all = 0; + if (! setup) + { + m_setup.clear(); + m_state_base.to_play = Color(0); + } + else + { + m_setup = *setup; + place_setup(m_setup); + m_state_base.to_play = setup->to_play; + optimize_attach_point_lists(); + for (Color c : get_colors()) + if (m_state_color[c].pieces_left.empty()) + m_state_color[c].points += m_bonus_all_pieces; + } + m_moves.clear(); +} + +void Board::init_variant(Variant variant) +{ + m_variant = variant; + m_nu_colors = libpentobi_base::get_nu_colors(variant); + if (m_nu_colors == 2) + { + m_color_name[Color(0)] = "Blue"; + m_color_name[Color(1)] = "Green"; + m_color_esc_sequence[Color(0)] = "\x1B[1;34;47m"; + m_color_esc_sequence[Color(1)] = "\x1B[1;32;47m"; + m_color_esc_sequence_text[Color(0)] = "\x1B[1;34m"; + m_color_esc_sequence_text[Color(1)] = "\x1B[1;32m"; + } + else + { + m_color_name[Color(0)] = "Blue"; + m_color_name[Color(1)] = "Yellow"; + m_color_name[Color(2)] = "Red"; + m_color_name[Color(3)] = "Green"; + m_color_esc_sequence[Color(0)] = "\x1B[1;34;47m"; + m_color_esc_sequence[Color(1)] = "\x1B[1;33;47m"; + m_color_esc_sequence[Color(2)] = "\x1B[1;31;47m"; + m_color_esc_sequence[Color(3)] = "\x1B[1;32;47m"; + m_color_esc_sequence_text[Color(0)] = "\x1B[1;34m"; + m_color_esc_sequence_text[Color(1)] = "\x1B[1;33m"; + m_color_esc_sequence_text[Color(2)] = "\x1B[1;31m"; + m_color_esc_sequence_text[Color(3)] = "\x1B[1;32m"; + } + m_nu_players = libpentobi_base::get_nu_players(variant); + m_bc = &BoardConst::get(variant); + m_piece_set = m_bc->get_piece_set(); + m_is_callisto = (m_piece_set == PieceSet::callisto); + if ((m_piece_set == PieceSet::classic && variant != Variant::junior) + || m_piece_set == PieceSet::trigon) + { + m_bonus_all_pieces = 15; + m_bonus_one_piece = 5; + } + else if (m_piece_set == PieceSet::nexos) + { + m_bonus_all_pieces = 10; + m_bonus_one_piece = 0; + } + else + { + m_bonus_all_pieces = 0; + m_bonus_one_piece = 0; + } + m_max_piece_size = m_bc->get_max_piece_size(); + m_max_adj_attach = m_bc->get_max_adj_attach(); + m_geo = &m_bc->get_geometry(); + m_move_info_array = m_bc->get_move_info_array(); + m_move_info_ext_array = m_bc->get_move_info_ext_array(); + m_move_info_ext_2_array = m_bc->get_move_info_ext_2_array(); + m_starting_points.init(variant, *m_geo); + if (m_piece_set == PieceSet::callisto) + for (Point p : *m_geo) + m_is_center_section[p] = + CallistoGeometry::is_center_section(m_geo->get_x(p), + m_geo->get_y(p), + m_nu_players); + else + m_is_center_section.fill(false, *m_geo); + for (Color c : get_colors()) + { + if (m_nu_players == 2 && m_nu_colors == 4) + m_second_color[c] = get_next(get_next(c)); + else + m_second_color[c] = c; + } + for (Piece::IntType i = 0; i < get_nu_uniq_pieces(); ++i) + { + Piece piece(i); + auto& piece_info = get_piece_info(piece); + m_score_points[piece] = piece_info.get_score_points(); + if (piece_info.get_points().size() == 1) + m_one_piece = piece; + } +} + +bool Board::is_game_over() const +{ + for (Color c : get_colors()) + if (has_moves(c)) + return false; + return true; +} + +bool Board::is_legal(Color c, Move mv) const +{ + auto piece = get_move_piece(mv); + if (! is_piece_left(c, piece)) + return false; + auto points = get_move_points(mv); + auto i = points.begin(); + auto end = points.end(); + bool has_attach_point = false; + do + { + if (m_state_color[c].forbidden[*i]) + return false; + if (is_attach_point(*i, c)) + has_attach_point = true; + } + while (++i != end); + if (m_is_callisto) + { + if (m_state_color[c].nu_left_piece[m_one_piece] > 1 + && piece != m_one_piece) + return false; + if (piece == m_one_piece) + return ! m_is_center_section[*points.begin()]; + } + if (has_attach_point) + return true; + if (! is_first_piece(c)) + return false; + i = points.begin(); + do + if (is_colorless_starting_point(*i) + || (is_colored_starting_point(*i) + && get_starting_point_color(*i) == c)) + return true; + while (++i != end); + return false; +} + +/** Remove forbidden points from attach point lists. + The attach point lists do not guarantee that they contain only + non-forbidden attach points because that would be too expensive to + update incrementally but at certain times that are not performance + critical (e.g. before taking a snapshot), we can remove them. */ +void Board::optimize_attach_point_lists() +{ + PointList l; + for (Color c : get_colors()) + { + l.clear(); + for (Point p : m_attach_points[c]) + if (! is_forbidden(p, c)) + l.push_back(p); + m_attach_points[c] = l; + } +} + +/** Place setup moves on board. */ +void Board::place_setup(const Setup& setup) +{ + if (m_max_piece_size == 5) + for (Color c : get_colors()) + for (Move mv : setup.placements[c]) + place<5, 16>(c, mv); + else if (m_max_piece_size == 6) + for (Color c : get_colors()) + for (Move mv : setup.placements[c]) + place<6, 22>(c, mv); + else + for (Color c : get_colors()) + for (Move mv : setup.placements[c]) + place<7, 12>(c, mv); +} + +void Board::play(Color c, Move mv) +{ + if (m_max_piece_size == 5) + play<5, 16>(c, mv); + else if (m_max_piece_size == 6) + play<6, 22>(c, mv); + else + play<7, 12>(c, mv); +} + +void Board::take_snapshot() +{ + optimize_attach_point_lists(); + m_snapshot.moves_size = m_moves.size(); + m_snapshot.state_base.to_play = m_state_base.to_play; + m_snapshot.state_base.nu_onboard_pieces_all = + m_state_base.nu_onboard_pieces_all; + m_snapshot.state_base.point_state.copy_from(m_state_base.point_state, + *m_geo); + for (Color c : get_colors()) + { + m_snapshot.attach_points_size[c] = m_attach_points[c].size(); + const auto& state = m_state_color[c]; + auto& snapshot_state = m_snapshot.state_color[c]; + snapshot_state.forbidden.copy_from(state.forbidden, *m_geo); + snapshot_state.is_attach_point.copy_from(state.is_attach_point, + *m_geo); + snapshot_state.pieces_left = state.pieces_left; + snapshot_state.nu_left_piece = state.nu_left_piece; + snapshot_state.nu_onboard_pieces = state.nu_onboard_pieces; + snapshot_state.points = state.points; + } +} + +void Board::write(ostream& out, bool mark_last_move) const +{ + // Sort lists of left pieces by name + ColorMap pieces_left; + for (Color c : get_colors()) + { + pieces_left[c] = m_state_color[c].pieces_left; + sort(pieces_left[c].begin(), pieces_left[c].end(), + [&](Piece p1, Piece p2) + { + return + get_piece_info(p1).get_name() + < get_piece_info(p2).get_name(); + }); + } + + ColorMove last_mv = ColorMove::null(); + if (mark_last_move) + { + unsigned n = get_nu_moves(); + if (n > 0) + last_mv = get_move(n - 1); + } + unsigned width = m_geo->get_width(); + unsigned height = m_geo->get_height(); + bool is_info_location_right = (width <= 20); + bool is_trigon = (m_piece_set == PieceSet::trigon); + bool is_nexos = (m_piece_set == PieceSet::nexos); + bool is_callisto = (m_piece_set == PieceSet::callisto); + for (unsigned y = 0; y < height; ++y) + { + if (height - y < 10) + out << ' '; + out << (height - y) << ' '; + for (unsigned x = 0; x < width; ++x) + { + Point p = m_geo->get_point(x, y); + bool is_offboard = p.is_null(); + auto point_type = m_geo->get_point_type(x, y); + if ((x > 0 || (is_trigon && x == 0 && m_geo->is_onboard(x + 1, y))) + && ! is_offboard) + { + // Print a space horizontally between fields on the board. On a + // Trigon board, a slash or backslash is used instead of the + // space to indicate the orientation of the triangles. A + // less-than/greater-than character is used instead of the + // space to mark the last piece played. + if (! last_mv.is_null() + && get_move_points(last_mv.move).contains(p) + && (x == 0 || ! m_geo->is_onboard(x - 1, y) + || get_point_state(m_geo->get_point(x - 1, y)) + != last_mv.color)) + { + set_color(out, "\x1B[1;37;47m"); + out << '>'; + last_mv = ColorMove::null(); + } + else if (! last_mv.is_null() + && x > 0 && m_geo->is_onboard(x - 1, y) + && get_move_points(last_mv.move).contains( + m_geo->get_point(x - 1, y)) + && get_point_state(p) != last_mv.color + && get_point_state(m_geo->get_point(x - 1, y)) + == last_mv.color) + { + set_color(out, "\x1B[1;37;47m"); + out << '<'; + last_mv = ColorMove::null(); + } + else if (is_trigon) + { + set_color(out, "\x1B[1;30;47m"); + out << (point_type == 1 ? '\\' : '/'); + } + else + { + set_color(out, "\x1B[1;30;47m"); + out << ' '; + } + } + if (is_offboard) + { + if (is_trigon && x > 0 && m_geo->is_onboard(x - 1, y)) + { + set_color(out, "\x1B[1;30;47m"); + out << (point_type == 1 ? '\\' : '/'); + } + else if (is_callisto && x == 0) + { + set_color(out, "\x1B[0m"); + out << ' '; + } + else + { + set_color(out, is_nexos ? "\x1B[1;30;47m" : "\x1B[0m"); + out << " "; + } + } + else + { + PointState s = get_point_state(p); + if (s.is_empty()) + { + if (is_colored_starting_point(p) && ! is_nexos) + { + Color c = get_starting_point_color(p); + set_color(out, m_color_esc_sequence[c]); + out << '+'; + } + else if (is_colorless_starting_point(p)) + { + set_color(out, "\x1B[1;30;47m"); + out << '+'; + } + else + { + set_color(out, "\x1B[1;30;47m"); + if (is_trigon) + out << ' '; + else if (is_nexos && point_type == 1) + out << '-'; + else if (is_nexos && point_type == 2) + out << '|'; + else if (is_nexos && point_type == 0) + out << '+'; + else if (is_callisto && is_center_section(p)) + out << ','; + else + out << '.'; + } + } + else + { + Color color = s.to_color(); + set_color(out, m_color_esc_sequence[color]); + if (is_nexos && m_geo->get_point_type(p) == 0) + out << '*'; // Uncrossable junction + else + out << m_color_char[color]; + } + } + } + if (is_trigon) + { + if (m_geo->is_onboard(width - 1, y)) + { + set_color(out, "\x1B[1;30;47m"); + out << (m_geo->get_point_type(width - 1, y) != 1 ? '\\' : '/'); + } + else + { + set_color(out, "\x1B[0m"); + out << " "; + } + } + set_color(out, "\x1B[0m"); + if (is_info_location_right) + write_info_line(out, y, pieces_left); + out << '\n'; + } + write_x_coord(out, width, is_trigon ? 3 : 2); + if (! is_info_location_right) + for (Color c : get_colors()) + { + write_color_info_line1(out, c); + out << " "; + write_color_info_line2(out, c, pieces_left[c]); + out << ' '; + write_color_info_line3(out, c, pieces_left[c]); + out << '\n'; + } +} + +void Board::write_color_info_line1(ostream& out, Color c) const +{ + set_color(out, m_color_esc_sequence_text[c]); + if (! is_game_over() && get_effective_to_play() == c) + out << '(' << (get_nu_moves() + 1) << ") "; + out << m_color_name[c] << "(" << m_color_char[c] << "): " << get_points(c); + if (! has_moves(c)) + out << '!'; + set_color(out, "\x1B[0m"); +} + +void Board::write_color_info_line2(ostream& out, Color c, + const PiecesLeftList& pieces_left) const +{ + if (m_variant == Variant::junior) + write_pieces_left(out, c, pieces_left, 0, 6); + else + write_pieces_left(out, c, pieces_left, 0, 10); +} + +void Board::write_color_info_line3(ostream& out, Color c, + const PiecesLeftList& pieces_left) const +{ + if (m_variant == Variant::junior) + write_pieces_left(out, c, pieces_left, 6, get_nu_uniq_pieces()); + else + write_pieces_left(out, c, pieces_left, 10, get_nu_uniq_pieces()); +} + +void Board::write_info_line(ostream& out, unsigned y, + const ColorMap& pieces_left) const +{ + if (y == 0) + { + out << " "; + write_color_info_line1(out, Color(0)); + } + else if (y == 1) + { + out << " "; + write_color_info_line2(out, Color(0), pieces_left[Color(0)]); + } + else if (y == 2) + { + out << " "; + write_color_info_line3(out, Color(0), pieces_left[Color(0)]); + } + else if (y == 4) + { + out << " "; + write_color_info_line1(out, Color(1)); + } + else if (y == 5) + { + out << " "; + write_color_info_line2(out, Color(1), pieces_left[Color(1)]); + } + else if (y == 6) + { + out << " "; + write_color_info_line3(out, Color(1), pieces_left[Color(1)]); + } + else if (y == 8 && m_nu_colors > 2) + { + out << " "; + write_color_info_line1(out, Color(2)); + } + else if (y == 9 && m_nu_colors > 2) + { + out << " "; + write_color_info_line2(out, Color(2), pieces_left[Color(2)]); + } + else if (y == 10 && m_nu_colors > 2) + { + out << " "; + write_color_info_line3(out, Color(2), pieces_left[Color(2)]); + } + else if (y == 12 && m_nu_colors > 3) + { + out << " "; + write_color_info_line1(out, Color(3)); + } + else if (y == 13 && m_nu_colors > 3) + { + out << " "; + write_color_info_line2(out, Color(3), pieces_left[Color(3)]); + } + else if (y == 14 && m_nu_colors > 3) + { + out << " "; + write_color_info_line3(out, Color(3), pieces_left[Color(3)]); + } +} + +void Board::write_pieces_left(ostream& out, Color c, + const PiecesLeftList& pieces_left, + unsigned begin, unsigned end) const +{ + for (unsigned i = begin; i < end; ++i) + if (i < pieces_left.size()) + { + if (i > begin) + out << ' '; + Piece piece = pieces_left[i]; + auto& name = get_piece_info(piece).get_name(); + unsigned nu_left = m_state_color[c].nu_left_piece[piece]; + for (unsigned j = 0; j < nu_left; ++j) + { + if (j > 0) + out << ' '; + out << name; + } + } +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/Board.h b/src/libpentobi_base/Board.h new file mode 100644 index 0000000..9217832 --- /dev/null +++ b/src/libpentobi_base/Board.h @@ -0,0 +1,901 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Board.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_BOARD_H +#define LIBPENTOBI_BASE_BOARD_H + +#include "BoardConst.h" +#include "ColorMap.h" +#include "ColorMove.h" +#include "Variant.h" +#include "Geometry.h" +#include "Grid.h" +#include "MoveList.h" +#include "PointList.h" +#include "PointState.h" +#include "Setup.h" +#include "StartingPoints.h" + +namespace libpentobi_base { + +class MoveMarker; + +//----------------------------------------------------------------------------- + +/** Blokus board. + @note @ref libboardgame_avoid_stack_allocation */ +class Board +{ +public: + typedef Grid PointStateGrid; + + /** Maximum number of pieces per player in any game variant. */ + static const unsigned max_pieces = Setup::max_pieces; + + typedef ArrayList PiecesLeftList; + + static const unsigned max_player_moves = max_pieces; + + /** Maximum number of moves in any game variant. */ + static const unsigned max_game_moves = Color::range * max_player_moves; + + /** Use ANSI escape sequences for colored text output in operator>> */ + static bool color_output; + + explicit Board(Variant variant); + + /** Not implemented to avoid unintended copies. + Use copy_from() to copy a board state. */ + Board(const Board&) = delete; + + /** Not implemented to avoid unintended copies. + Use copy_from() to copy a board state. */ + Board& operator=(const Board&) = delete; + + Geometry::Iterator begin() const { return m_geo->begin(); } + + Geometry::Iterator end() const { return m_geo->end(); } + + Variant get_variant() const; + + Color::IntType get_nu_colors() const; + + Color::Range get_colors() const { return Color::Range(m_nu_colors); } + + /** Number of colors that are not played alternately. + This is equal to get_nu_colors() apart from Variant::classic_3. */ + Color::IntType get_nu_nonalt_colors() const; + + unsigned get_nu_players() const; + + Piece::IntType get_nu_uniq_pieces() const; + + /** Number of instances of a unique piece per color. */ + unsigned get_nu_piece_instances(Piece piece) const; + + Color get_next(Color c) const; + + Color get_previous(Color c) const; + + const PieceTransforms& get_transforms() const; + + /** Get the state of an on-board point. */ + PointState get_point_state(Point p) const; + + const PointStateGrid& get_point_state() const; + + /** Get next color to play. + The next color to play is the next color of the color of the last move + played even if it has no more moves to play. */ + Color get_to_play() const; + + /** Get the player who plays the next move for the 4th color in + Variant::classic_3. */ + Color::IntType get_alt_player() const; + + /** Equivalent to get_effective_to_play(get_to_play()) */ + Color get_effective_to_play() const; + + /** Get next color to play that still has moves. + Colors are tried in their playing order starting with c. If no color + has moves left, c is returned. */ + Color get_effective_to_play(Color c) const; + + const PiecesLeftList& get_pieces_left(Color c) const; + + bool is_piece_left(Color c, Piece piece) const; + + /** Check if no piece of a color has been placed on the board yet. + This includes setup pieces and played moves. */ + bool is_first_piece(Color c) const; + + /** Get number of instances left of a piece. + This value can be greater 1 in game variants that use multiple instances + of a unique piece per player. */ + unsigned get_nu_left_piece(Color c, Piece piece) const; + + /** Get number of points of a color including the bonus. */ + ScoreType get_points(Color c) const { return m_state_color[c].points; } + + /** Get number of bonus points of a color. */ + ScoreType get_bonus(Color c) const; + + /** Is a point a potential attachment point for a color. + Does not check if the point is forbidden. */ + bool is_attach_point(Point p, Color c) const; + + /** Get potential attachment points for a color. + Does not check if the point is forbidden. */ + const PointList& get_attach_points(Color c) const; + + /** Initialize the current board for a given game variant. + @param variant The game variant + @param setup An optional setup position to initialize the board + with. */ + void init(Variant variant, const Setup* setup = nullptr); + + /** Clear the current board without changing the current game variant. + See init(Variant,const Setup*) */ + void init(const Setup* setup = nullptr); + + /** Copy the board state and move history from another board. + This is like an assignment operator but because boards are rarely + copied by value and copying is expensive, it is an explicit function to + avoid accidental copying. */ + void copy_from(const Board& bd); + + /** Play a move. + @pre ! mv.is_null() + @pre get_nu_moves() < max_game_moves */ + void play(Color c, Move mv); + + /** More efficient version of play() if maximum piece size of current + game variant is known at compile time. */ + template + void play(Color c, Move mv); + + /** Play a move. + @pre ! mv.move.is_null() + @pre get_nu_moves() < max_game_moves */ + void play(ColorMove mv); + + void set_to_play(Color c); + + void write(ostream& out, bool mark_last_move = true) const; + + /** Get the setup of the board before any moves were played. + If the board was initialized without setup, the return value contains + a setup with empty placement lists and Color(0) as the color to + play. */ + const Setup& get_setup() const; + + bool has_setup() const; + + /** Get the total number of moves played by all colors. + Does not include setup pieces. + @see get_nu_onboard_pieces() */ + unsigned get_nu_moves() const; + + /** Get the number of pieces on board. + This is the number of setup pieces, if the board was initialized + with a setup position, plus the number of pieces played as moves. */ + unsigned get_nu_onboard_pieces() const; + + /** Get the number of pieces on board of a color. + This is the number of setup pieces, if the board was initialized + with a setup position, plus the number of pieces played as moves. */ + unsigned get_nu_onboard_pieces(Color c) const; + + ColorMove get_move(unsigned n) const; + + const ArrayList& get_moves() const; + + /** Generate all legal moves for a color. + @param c The color + @param marker A move marker reused for efficiency (needs to be clear) + @param[out] moves The list of moves. */ + void gen_moves(Color c, MoveMarker& marker, MoveList& moves) const; + + bool has_moves(Color c) const; + + /** Check that no color has any moves left. */ + bool is_game_over() const; + + /** Check if a move is legal. + @pre ! mv.is_null() */ + bool is_legal(Color c, Move mv) const; + + /** Check if a move is legal for the current color to play. + @pre ! mv.is_null() */ + bool is_legal(Move mv) const; + + /** Check that point is not already occupied or adjacent to own color. + Point::null() is an allowed argument and returns false. */ + bool is_forbidden(Point p, Color c) const; + + const GridExt& is_forbidden(Color c) const; + + /** Check that no points of move are already occupied or adjacent to own + color. + Does not check if the move is diagonally adjacent to an existing + occupied point of the same color. */ + bool is_forbidden(Color c, Move mv) const; + + const BoardConst& get_board_const() const { return *m_bc; } + + BoardType get_board_type() const; + + PieceSet get_piece_set() const { return m_piece_set; } + + unsigned get_adj_status(Point p, Color c) const; + + /** Is a point in the center section that is forbidden for the 1-piece in + Callisto? + Always returns false for other game variants. */ + bool is_center_section(Point p) const { return m_is_center_section[p]; } + + PrecompMoves::Range get_moves(Piece piece, Point p, + unsigned adj_status) const; + + /** Get score. + The score is the number of points for a color minus the number of + points of the opponent (or the average score of the opponents if there + are more than two players). */ + ScoreType get_score(Color c) const; + + /** Specialized version of get_score(). + @pre get_nu_colors() == 2 */ + ScoreType get_score_twocolor(Color c) const; + + /** Specialized version of get_score(). + @pre get_nu_players() == 4 && get_nu_colors() == 4 */ + ScoreType get_score_multicolor(Color c) const; + + /** Specialized version of get_score(). + @pre get_nu_players() > 2 */ + ScoreType get_score_multiplayer(Color c) const; + + /** Specialized version of get_score(). + @pre get_nu_players() == 2 */ + ScoreType get_score_twoplayer(Color c) const; + + /** Get the place of a player in the game result. + @param c The color of the player. + @param[out] place The place of the player with that color. The place + numbers start with 0. A place can be shared if several players have the + same score. If a place is shared by n players, the following n-1 places + are not used. + @param[out] is_shared True if the place was shared. */ + void get_place(Color c, unsigned& place, bool& is_shared) const; + + const Geometry& get_geometry() const { return *m_geo; } + + /** See BoardConst::to_string() */ + string to_string(Move mv, bool with_piece_name = false) const; + + /** See BoardConst::from_string() */ + Move from_string(const string& s) const; + + bool find_move(const MovePoints& points, Move& mv) const; + + bool find_move(const MovePoints& points, Piece piece, Move& mv) const; + + const Transform* find_transform(Move mv) const; + + const PieceInfo& get_piece_info(Piece piece) const; + + bool get_piece_by_name(const string& name, Piece& piece) const; + + /** The 1x1 piece. */ + Piece get_one_piece() const { return m_one_piece; } + + Range get_move_points(Move mv) const; + + Piece get_move_piece(Move mv) const; + + const MoveInfoExt2& get_move_info_ext_2(Move mv) const; + + bool is_colored_starting_point(Point p) const; + + bool is_colorless_starting_point(Point p) const; + + Color get_starting_point_color(Point p) const; + + const ArrayList& + get_starting_points(Color c) const; + + /** Get the second color in game variants in which a player plays two + colors. + @return The second color of the player that plays color c, or c if + the player plays only one color in the current game variant or + if the game variant is classic_3. */ + Color get_second_color(Color c) const; + + bool is_same_player(Color c1, Color c2) const; + + Move get_move_at(Point p) const; + + /** Remember the board state to quickly restore it later. + A snapshot can only be restored from a position that was reached + after playing moves from the snapshot position. */ + void take_snapshot(); + + /** See take_snapshot() */ + void restore_snapshot(); + +private: + /** Color-independent part of the board state. */ + struct StateBase + { + Color to_play; + + unsigned nu_onboard_pieces_all; + + PointStateGrid point_state; + }; + + /** Color-dependent part of the board state. */ + struct StateColor + { + GridExt forbidden; + + Grid is_attach_point; + + PiecesLeftList pieces_left; + + PieceMap nu_left_piece; + + unsigned nu_onboard_pieces; + + ScoreType points; + }; + + /** Snapshot for fast restoration of a previous position. */ + struct Snapshot + { + StateBase state_base; + + ColorMap state_color; + + unsigned moves_size; + + ColorMap attach_points_size; + }; + + + StateBase m_state_base; + + ColorMap m_state_color; + + Variant m_variant; + + PieceSet m_piece_set; + + Color::IntType m_nu_colors; + + bool m_is_callisto; + + unsigned m_nu_players; + + /** Caches m_bc->get_max_piece_size(). */ + unsigned m_max_piece_size; + + /** Caches m_bc->get_max_adj_attach(). */ + unsigned m_max_adj_attach; + + /** Bonus for playing all pieces. */ + ScoreType m_bonus_all_pieces; + + /** Bonus for playing the 1-piece last. */ + ScoreType m_bonus_one_piece; + + /** Caches get_piece_info(piece).get_score_points() */ + PieceMap m_score_points; + + const BoardConst* m_bc; + + /** Caches m_bc->get_move_info_array() */ + BoardConst::MoveInfoArray m_move_info_array; + + /** Caches m_bc->get_move_info_ext_array() */ + BoardConst::MoveInfoExtArray m_move_info_ext_array; + + /** Caches m_bc->get_move_info_ext_2_array() */ + const MoveInfoExt2* m_move_info_ext_2_array; + + const Geometry* m_geo; + + /** See is_center_section(). */ + Grid m_is_center_section; + + /** The 1x1 piece. */ + Piece m_one_piece; + + ColorMap m_attach_points; + + /** See get_second_color() */ + ColorMap m_second_color; + + ColorMap m_color_char; + + ColorMap m_color_esc_sequence; + + ColorMap m_color_esc_sequence_text; + + ColorMap m_color_name; + + ArrayList m_moves; + + Snapshot m_snapshot; + + Setup m_setup; + + StartingPoints m_starting_points; + + + void gen_moves(Color c, Point p, Piece piece, unsigned adj_status, + MoveMarker& marker, MoveList& moves) const; + + bool has_moves(Color c, Point p) const; + + void init_variant(Variant variant); + + void optimize_attach_point_lists(); + + template + void place(Color c, Move mv); + + void place_setup(const Setup& setup); + + void write_pieces_left(ostream& out, Color c, + const PiecesLeftList& pieces_left, unsigned begin, + unsigned end) const; + + void write_color_info_line1(ostream& out, Color c) const; + + void write_color_info_line2(ostream& out, Color c, + const PiecesLeftList& pieces_left) const; + + void write_color_info_line3(ostream& out, Color c, + const PiecesLeftList& pieces_left) const; + + void write_info_line(ostream& out, unsigned y, + const ColorMap& pieces_left) const; +}; + + +inline bool Board::find_move(const MovePoints& points, Move& mv) const +{ + return m_bc->find_move(points, mv); +} + +inline bool Board::find_move(const MovePoints& points, Piece piece, + Move& mv) const +{ + return m_bc->find_move(points, piece, mv); +} + +inline Move Board::from_string(const string& s) const +{ + return m_bc->from_string(s); +} + +inline unsigned Board::get_adj_status(Point p, Color c) const +{ + auto i = m_bc->get_adj_status_list(p).begin(); + unsigned result = is_forbidden(*i, c); // bool converted to integer is 1 + for (unsigned j = 1; j < PrecompMoves::adj_status_nu_adj; ++j) + result |= (is_forbidden(*(++i), c) << j); + return result; +} + +inline Color::IntType Board::get_alt_player() const +{ + LIBBOARDGAME_ASSERT(m_variant == Variant::classic_3); + return static_cast(get_nu_onboard_pieces(Color(3)) % 3); +} + +inline const PointList& Board::get_attach_points(Color c) const +{ + return m_attach_points[c]; +} + +inline BoardType Board::get_board_type() const +{ + return m_bc->get_board_type(); +} + +inline ColorMove Board::get_move(unsigned n) const +{ + return m_moves[n]; +} + +inline const MoveInfoExt2& Board::get_move_info_ext_2(Move mv) const +{ + LIBBOARDGAME_ASSERT(! mv.is_null()); + LIBBOARDGAME_ASSERT(mv.to_int() < m_bc->get_nu_moves()); + return *(m_move_info_ext_2_array + mv.to_int()); +} + +inline Piece Board::get_move_piece(Move mv) const +{ + return m_bc->get_move_piece(mv); +} + +inline Range Board::get_move_points(Move mv) const +{ + return m_bc->get_move_points(mv); +} + +inline auto Board::get_moves() const +-> const ArrayList& +{ + return m_moves; +} + +inline PrecompMoves::Range Board::get_moves(Piece piece, Point p, + unsigned adj_status) const +{ + return m_bc->get_moves(piece, p, adj_status); +} + +inline Color Board::get_next(Color c) const +{ + return c.get_next(m_nu_colors); +} + +inline Color::IntType Board::get_nu_colors() const +{ + return m_nu_colors; +} + +inline unsigned Board::get_nu_left_piece(Color c, Piece piece) const +{ + LIBBOARDGAME_ASSERT(piece.to_int() < get_nu_uniq_pieces()); + return m_state_color[c].nu_left_piece[piece]; +} + +inline unsigned Board::get_nu_moves() const +{ + return m_moves.size(); +} + +inline Color::IntType Board::get_nu_nonalt_colors() const +{ + return m_variant != Variant::classic_3 ? m_nu_colors : 3; +} + +inline unsigned Board::get_nu_onboard_pieces() const +{ + return m_state_base.nu_onboard_pieces_all; +} + +inline unsigned Board::get_nu_onboard_pieces(Color c) const +{ + return m_state_color[c].nu_onboard_pieces; +} + +inline unsigned Board::get_nu_players() const +{ + return m_nu_players; +} + +inline unsigned Board::get_nu_piece_instances(Piece piece) const +{ + return m_bc->get_piece_info(piece).get_nu_instances(); +} + +inline Piece::IntType Board::get_nu_uniq_pieces() const +{ + return m_bc->get_nu_pieces(); +} + +inline const PieceInfo& Board::get_piece_info(Piece piece) const +{ + return m_bc->get_piece_info(piece); +} + +inline bool Board::get_piece_by_name(const string& name, Piece& piece) const +{ + return m_bc->get_piece_by_name(name, piece); +} + +inline const Board::PiecesLeftList& Board::get_pieces_left(Color c) const +{ + return m_state_color[c].pieces_left; +} + +inline PointState Board::get_point_state(Point p) const +{ + return PointState(m_state_base.point_state[p].to_int()); +} + +inline const Board::PointStateGrid& Board::get_point_state() const +{ + return m_state_base.point_state; +} + +inline Color Board::get_previous(Color c) const +{ + return c.get_previous(m_nu_colors); +} + +inline ScoreType Board::get_score(Color c) const +{ + if (m_nu_colors == 2) + return get_score_twocolor(c); + else if (m_nu_players == 2) + return get_score_multicolor(c); + else + return get_score_multiplayer(c); +} + +inline ScoreType Board::get_score_twocolor(Color c) const +{ + LIBBOARDGAME_ASSERT(m_nu_colors == 2); + auto points0 = get_points(Color(0)); + auto points1 = get_points(Color(1)); + if (c == Color(0)) + return points0 - points1; + else + return points1 - points0; +} + +inline ScoreType Board::get_score_twoplayer(Color c) const +{ + LIBBOARDGAME_ASSERT(m_nu_players == 2); + if (m_nu_colors == 2) + return get_score_twocolor(c); + else + return get_score_multicolor(c); +} + +inline ScoreType Board::get_score_multicolor(Color c) const +{ + LIBBOARDGAME_ASSERT(m_nu_players == 2 && m_nu_colors == 4); + auto points0 = get_points(Color(0)) + get_points(Color(2)); + auto points1 = get_points(Color(1)) + get_points(Color(3)); + if (c == Color(0) || c == Color(2)) + return points0 - points1; + else + return points1 - points0; +} + +inline ScoreType Board::get_score_multiplayer(Color c) const +{ + LIBBOARDGAME_ASSERT(m_nu_players > 2); + ScoreType score = 0; + auto nu_players = static_cast(m_nu_players); + for (Color i : get_colors()) + if (i != c) + score -= get_points(i); + score = get_points(c) + score / (static_cast(nu_players) - 1); + return score; +} + +inline Color Board::get_second_color(Color c) const +{ + return m_second_color[c]; +} + +inline const Setup& Board::get_setup() const +{ + return m_setup; +} + +inline Color Board::get_starting_point_color(Point p) const +{ + return m_starting_points.get_starting_point_color(p); +} + +inline const ArrayList& + Board::get_starting_points(Color c) const +{ + return m_starting_points.get_starting_points(c); +} + +inline Color Board::get_to_play() const +{ + return m_state_base.to_play; +} + +inline const PieceTransforms& Board::get_transforms() const +{ + return m_bc->get_transforms(); +} + +inline Variant Board::get_variant() const +{ + return m_variant; +} + +inline void Board::init(const Setup* setup) +{ + init(m_variant, setup); +} + +inline bool Board::is_attach_point(Point p, Color c) const +{ + return m_state_color[c].is_attach_point[p]; +} + +inline bool Board::is_colored_starting_point(Point p) const +{ + return m_starting_points.is_colored_starting_point(p); +} + +inline bool Board::is_colorless_starting_point(Point p) const +{ + return m_starting_points.is_colorless_starting_point(p); +} + +inline bool Board::is_first_piece(Color c) const +{ + return m_state_color[c].nu_onboard_pieces == 0; +} + +inline bool Board::is_forbidden(Point p, Color c) const +{ + return m_state_color[c].forbidden[p]; +} + +inline const GridExt& Board::is_forbidden(Color c) const +{ + return m_state_color[c].forbidden; +} + +inline bool Board::is_forbidden(Color c, Move mv) const +{ + auto points = get_move_points(mv); + auto i = points.begin(); + auto end = points.end(); + do + if (m_state_color[c].forbidden[*i]) + return true; + while (++i != end); + return false; +} + +inline bool Board::is_legal(Move mv) const +{ + return is_legal(m_state_base.to_play, mv); +} + +inline bool Board::is_piece_left(Color c, Piece piece) const +{ + LIBBOARDGAME_ASSERT(piece.to_int() < get_nu_uniq_pieces()); + return m_state_color[c].nu_left_piece[piece] > 0; +} + +inline bool Board::is_same_player(Color c1, Color c2) const +{ + return c1 == c2 || c1 == m_second_color[c2]; +} + +template +inline void Board::place(Color c, Move mv) +{ + LIBBOARDGAME_ASSERT(m_max_piece_size == MAX_SIZE); + LIBBOARDGAME_ASSERT(m_max_adj_attach == MAX_ADJ_ATTACH); + auto& info = BoardConst::get_move_info(mv, m_move_info_array); + auto& info_ext = BoardConst::get_move_info_ext( + mv, m_move_info_ext_array); + auto piece = info.get_piece(); + auto& state_color = m_state_color[c]; + LIBBOARDGAME_ASSERT(state_color.nu_left_piece[piece] > 0); + auto score_points = m_score_points[piece]; + if (--state_color.nu_left_piece[piece] == 0) + { + state_color.pieces_left.remove_fast(piece); + if (state_color.pieces_left.empty()) + { + state_color.points += m_bonus_all_pieces; + if (MAX_SIZE == 7) // Nexos + LIBBOARDGAME_ASSERT(m_bonus_one_piece == 0); + else if (score_points == 1) + state_color.points += m_bonus_one_piece; + } + } + ++m_state_base.nu_onboard_pieces_all; + ++state_color.nu_onboard_pieces; + state_color.points += score_points; + auto i = info.begin(); + auto end = info.end(); + do + { + m_state_base.point_state[*i] = PointState(c); + for_each_color([&](Color c) { + m_state_color[c].forbidden[*i] = true; + }); + } + while (++i != end); + if (MAX_SIZE == 7) // Nexos + { + LIBBOARDGAME_ASSERT(info_ext.size_adj_points == 0); + i = info_ext.begin_attach(); + end = i + info_ext.size_attach_points; + } + else + { + end = info_ext.end_adj(); + for (i = info_ext.begin_adj(); i != end; ++i) + state_color.forbidden[*i] = true; + LIBBOARDGAME_ASSERT(i == info_ext.begin_attach()); + end += info_ext.size_attach_points; + } + auto& attach_points = m_attach_points[c]; + auto n = attach_points.size(); + do + if (! state_color.forbidden[*i] && ! state_color.is_attach_point[*i]) + { + state_color.is_attach_point[*i] = true; + attach_points.get_unchecked(n) = *i; + ++n; + } + while (++i != end); + attach_points.resize(n); +} + +template +inline void Board::play(Color c, Move mv) +{ + place(c, mv); + m_moves.push_back(ColorMove(c, mv)); + m_state_base.to_play = get_next(c); +} + +inline void Board::play(ColorMove mv) +{ + play(mv.color, mv.move); +} + +inline void Board::restore_snapshot() +{ + LIBBOARDGAME_ASSERT(m_snapshot.moves_size <= m_moves.size()); + auto& geo = get_geometry(); + m_moves.resize(m_snapshot.moves_size); + m_state_base.to_play = m_snapshot.state_base.to_play; + m_state_base.nu_onboard_pieces_all = + m_snapshot.state_base.nu_onboard_pieces_all; + m_state_base.point_state.memcpy_from(m_snapshot.state_base.point_state, + geo); + for (Color c : get_colors()) + { + const auto& snapshot_state = m_snapshot.state_color[c]; + auto& state = m_state_color[c]; + state.forbidden.copy_from(snapshot_state.forbidden, geo); + state.is_attach_point.copy_from(snapshot_state.is_attach_point, geo); + state.pieces_left = snapshot_state.pieces_left; + state.nu_left_piece = snapshot_state.nu_left_piece; + state.nu_onboard_pieces = snapshot_state.nu_onboard_pieces; + state.points = snapshot_state.points; + m_attach_points[c].resize(m_snapshot.attach_points_size[c]); + } +} + +inline void Board::set_to_play(Color c) +{ + m_state_base.to_play = c; +} + +inline string Board::to_string(Move mv, bool with_piece_name) const +{ + return m_bc->to_string(mv, with_piece_name); +} + +//----------------------------------------------------------------------------- + +inline ostream& operator<<(ostream& out, const Board& bd) +{ + bd.write(out); + return out; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_BOARD_H diff --git a/src/libpentobi_base/BoardConst.cpp b/src/libpentobi_base/BoardConst.cpp new file mode 100644 index 0000000..3ca651b --- /dev/null +++ b/src/libpentobi_base/BoardConst.cpp @@ -0,0 +1,1128 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/BoardConst.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "BoardConst.h" + +#include +#include "Marker.h" +#include "PieceTransformsClassic.h" +#include "PieceTransformsTrigon.h" +#include "libboardgame_base/Transform.h" +#include "libboardgame_util/Log.h" +#include "libboardgame_util/StringUtil.h" + +namespace libpentobi_base { + +using libboardgame_base::Transform; +using libboardgame_util::split; +using libboardgame_util::to_lower; +using libboardgame_util::trim; + +//----------------------------------------------------------------------------- + +namespace { + +const bool log_move_creation = false; + +/** Local variable used during construction. + Making this variable global slightly speeds up construction and a + thread-safe construction is not needed. */ +Marker g_marker; + +/** Non-compact representation of lists of moves of a piece at a point + constrained by the forbidden status of adjacent points. + Only used during construction. See g_marker why this variable is global. */ +Grid, PrecompMoves::nu_adj_status>> + g_full_move_table; + + +bool is_reverse(MovePoints::const_iterator begin1, const Point* begin2, unsigned size) +{ + auto j = begin2 + size - 1; + for (auto i = begin1; i != begin1 + size; ++i, --j) + if (*i != *j) + return false; + return true; +} + +// Sort points using the ordering used in blksgf files (switches the direction +// of the y axis!) +void sort_piece_points(PiecePoints& points) +{ + auto check = [&](unsigned short a, unsigned short b) + { + if ((points[a].y == points[b].y && points[a].x > points[b].x) + || points[a].y < points[b].y) + swap(points[a], points[b]); + }; + // Minimal number of necessary comparisons with sorting networks + auto size = points.size(); + switch (size) + { + case 7: + check(1, 2); + check(3, 4); + check(5, 6); + check(0, 2); + check(3, 5); + check(4, 6); + check(0, 1); + check(4, 5); + check(2, 6); + check(0, 4); + check(1, 5); + check(0, 3); + check(2, 5); + check(1, 3); + check(2, 4); + check(2, 3); + break; + case 6: + check(1, 2); + check(4, 5); + check(0, 2); + check(3, 5); + check(0, 1); + check(3, 4); + check(2, 5); + check(0, 3); + check(1, 4); + check(2, 4); + check(1, 3); + check(2, 3); + break; + case 5: + check(0, 1); + check(3, 4); + check(2, 4); + check(2, 3); + check(1, 4); + check(0, 3); + check(0, 2); + check(1, 3); + check(1, 2); + break; + case 4: + check(0, 1); + check(2, 3); + check(0, 2); + check(1, 3); + check(1, 2); + break; + case 3: + check(1, 2); + check(0, 2); + check(0, 1); + break; + case 2: + check(0, 1); + break; + default: + LIBBOARDGAME_ASSERT(size == 1); + } +} + +vector create_pieces_callisto(const Geometry& geo, + PieceSet piece_set, + const PieceTransforms& transforms) +{ + vector pieces; + pieces.reserve(19); + pieces.emplace_back("1", + PiecePoints{ CoordPoint(0, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0), 3); + pieces.emplace_back("W", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(-1, -1), + CoordPoint(0, 0), CoordPoint(0, 1), + CoordPoint(1, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("X", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, -1), + CoordPoint(0, 0), CoordPoint(0, 1), + CoordPoint(1, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("T5", + PiecePoints{ CoordPoint(-1, -1), CoordPoint(0, 1), + CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(1, -1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("U", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(-1, -1), + CoordPoint(0, 0), CoordPoint(1, 0), + CoordPoint(1, -1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("L", + PiecePoints{ CoordPoint(0, 1), CoordPoint(0, 0), + CoordPoint(0, -1), CoordPoint(1, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("T4", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0), + CoordPoint(1, 0), CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("Z", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0), + CoordPoint(0, 1), CoordPoint(1, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("O", + PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(1, 0), CoordPoint(1, -1) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("V", + PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(1, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("I", + PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0), + CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("2", + PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + return pieces; +} + +vector create_pieces_classic(const Geometry& geo, + PieceSet piece_set, + const PieceTransforms& transforms) +{ + vector pieces; + // Define the 21 standard pieces. The piece names are the standard names as + // in http://blokusstrategy.com/?p=48. The default orientation is chosen + // such that it resembles the letter. + pieces.reserve(21); + pieces.emplace_back("V5", + PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(0, -2), CoordPoint(1, 0), + CoordPoint(2, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("L5", + PiecePoints{ CoordPoint(0, 1), CoordPoint(1, 1), + CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(0, -2) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("Z5", + PiecePoints{ CoordPoint(-1, -1), CoordPoint(0, 1), + CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(1, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("N", + PiecePoints{ CoordPoint(-1, 1), CoordPoint(-1, 0), + CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(0, -2)}, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("W", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(-1, -1), + CoordPoint(0, 0), CoordPoint(0, 1), + CoordPoint(1, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("X", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, -1), + CoordPoint(0, 0), CoordPoint(0, 1), + CoordPoint(1, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("F", + PiecePoints{ CoordPoint(0, -1), CoordPoint(1, -1), + CoordPoint(-1, 0), CoordPoint(0, 0), + CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("I5", + PiecePoints{ CoordPoint(0, 2), CoordPoint(0, 1), + CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(0, -2) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("T5", + PiecePoints{ CoordPoint(-1, -1), CoordPoint(0, 1), + CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(1, -1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("Y", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0), + CoordPoint(0, -1), CoordPoint(0, 1), + CoordPoint(0, 2) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("P", + PiecePoints{ CoordPoint(0, 1), CoordPoint(0, 0), + CoordPoint(0, -1), CoordPoint(1, 0), + CoordPoint(1, -1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("U", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(-1, -1), + CoordPoint(0, 0), CoordPoint(1, 0), + CoordPoint(1, -1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("L4", + PiecePoints{ CoordPoint(0, 1), CoordPoint(0, 0), + CoordPoint(0, -1), CoordPoint(1, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("I4", + PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0), + CoordPoint(0, 1), CoordPoint(0, 2) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("T4", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0), + CoordPoint(1, 0), CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("Z4", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0), + CoordPoint(0, 1), CoordPoint(1, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("O", + PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(1, 0), CoordPoint(1, -1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("V3", + PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(1, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("I3", + PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0), + CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("2", + PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("1", + PiecePoints{ CoordPoint(0, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + return pieces; +} + +vector create_pieces_junior(const Geometry& geo, + PieceSet piece_set, + const PieceTransforms& transforms) +{ + vector pieces; + pieces.reserve(12); + pieces.emplace_back("L5", + PiecePoints{ CoordPoint(0, 1), CoordPoint(1, 1), + CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(0, -2) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("P", + PiecePoints{ CoordPoint(0, 1), CoordPoint(0, 0), + CoordPoint(0, -1), CoordPoint(1, 0), + CoordPoint(1, -1) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("I5", + PiecePoints{ CoordPoint(0, 2), CoordPoint(0, 1), + CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(0, -2) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("O", + PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(1, 0), CoordPoint(1, -1) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("T4", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0), + CoordPoint(1, 0), CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("Z4", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0), + CoordPoint(0, 1), CoordPoint(1, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("L4", + PiecePoints{ CoordPoint(0, 1), CoordPoint(0, 0), + CoordPoint(0, -1), CoordPoint(1, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("I4", + PiecePoints{ CoordPoint(0, 1), CoordPoint(0, 0), + CoordPoint(0, -1), CoordPoint(0, -2) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("V3", + PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1), + CoordPoint(1, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("I3", + PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0), + CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("2", + PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + pieces.emplace_back("1", + PiecePoints{ CoordPoint(0, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0), 2); + return pieces; +} + +vector create_pieces_trigon(const Geometry& geo, + PieceSet piece_set, + const PieceTransforms& transforms) +{ + vector pieces; + // Define the 22 standard Trigon pieces. The piece names are similar to one + // of the possible notations from the thread "Trigon book: how to play, how + // to win" from August 2010 in the Blokus forums + // http://forum.blokus.refreshed.be/viewtopic.php?f=2&t=2539#p9867 + // apart from that the smallest pieces are named '2' and '1' like in + // Classic to avoid to many pieces with letter 'I' and that numbers are + // only used if there is more than one piece with the same letter. + pieces.reserve(22); + pieces.emplace_back("I6", + PiecePoints{ CoordPoint(1, -1), CoordPoint(2, -1), + CoordPoint(0, 0), CoordPoint(1, 0), + CoordPoint(-1, 1), CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("L6", + PiecePoints{ CoordPoint(1, -1), CoordPoint(2, -1), + CoordPoint(0, 0), CoordPoint(1, 0), + CoordPoint(0, 1), CoordPoint(1, 1) }, + geo, transforms, piece_set, CoordPoint(1, 0)); + pieces.emplace_back("V", + PiecePoints{ CoordPoint(-2, -1), CoordPoint(-1, -1), + CoordPoint(-1, 0), CoordPoint(0, 0), + CoordPoint(1, 0), CoordPoint(2, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("S", + PiecePoints{ CoordPoint(-1, -1), CoordPoint(0, -1), + CoordPoint(-1, 0), CoordPoint(0, 0), + CoordPoint(-1, 1), CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("P6", + PiecePoints{ CoordPoint(1, -1), CoordPoint(0, 0), + CoordPoint(1, 0), CoordPoint(2, 0), + CoordPoint(-1, 1), CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(1, 0)); + pieces.emplace_back("F", + PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0), + CoordPoint(0, 1), CoordPoint(1, 1), + CoordPoint(2, 1), CoordPoint(1, 2) }, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("W", + PiecePoints{ CoordPoint(1, -1), CoordPoint(-1, 0), + CoordPoint(0, 0), CoordPoint(1, 0), + CoordPoint(2, 0), CoordPoint(3, 0) }, + geo, transforms, piece_set, CoordPoint(1, 0)); + pieces.emplace_back("A6", + PiecePoints{ CoordPoint(1, -1), CoordPoint(0, 0), + CoordPoint(1, 0), CoordPoint(2, 0), + CoordPoint(0, 1), CoordPoint(2, 1) }, + geo, transforms, piece_set, CoordPoint(1, 0)); + pieces.emplace_back("G", + PiecePoints{ CoordPoint(1, -1), CoordPoint(0, 0), + CoordPoint(1, 0), CoordPoint(0, 1), + CoordPoint(1, 1), CoordPoint(2, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("Y", + PiecePoints{ CoordPoint(-1, -1), CoordPoint(-1, 0), + CoordPoint(0, 0), CoordPoint(1, 0), + CoordPoint(-1, 1), CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("X", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0), + CoordPoint(1, 0), CoordPoint(-1, 1), + CoordPoint(0, 1), CoordPoint(1, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("O", + PiecePoints{ CoordPoint(-1, -1), CoordPoint(0, -1), + CoordPoint(1, -1), CoordPoint(-1, 0), + CoordPoint(0, 0), CoordPoint(1, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("I5", + PiecePoints{ CoordPoint(1, -1), CoordPoint(0, 0), + CoordPoint(1, 0), CoordPoint(-1, 1), + CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("L5", + PiecePoints{ CoordPoint(1, -1), CoordPoint(0, 0), + CoordPoint(1, 0), CoordPoint(0, 1), + CoordPoint(1, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("C5", + PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0), + CoordPoint(0, 1), CoordPoint(1, 1), + CoordPoint(2, 1) }, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("P5", + PiecePoints{ CoordPoint(1, -1), CoordPoint(0, 0), + CoordPoint(1, 0), CoordPoint(2, 0), + CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(1, 0)); + pieces.emplace_back("I4", + PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0), + CoordPoint(-1, 1), CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("C4", + PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0), + CoordPoint(0, 1), CoordPoint(1, 1) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("A4", + PiecePoints{ CoordPoint(1, -1), CoordPoint(0, 0), + CoordPoint(1, 0), CoordPoint(2, 0) }, + geo, transforms, piece_set, CoordPoint(1, 0)); + pieces.emplace_back("I3", + PiecePoints{ CoordPoint(1, -1), CoordPoint(0, 0), + CoordPoint(1, 0) }, + geo, transforms, piece_set, CoordPoint(1, 0)); + pieces.emplace_back("2", + PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + pieces.emplace_back("1", + PiecePoints{ CoordPoint(0, 0) }, + geo, transforms, piece_set, CoordPoint(0, 0)); + return pieces; +} + +vector create_pieces_nexos(const Geometry& geo, + PieceSet piece_set, + const PieceTransforms& transforms) +{ + vector pieces; + pieces.reserve(24); + pieces.emplace_back("I4", + PiecePoints{ CoordPoint(0, -3), CoordPoint(0, -2), + CoordPoint(0, -1), CoordPoint(0, 0), + CoordPoint(0, 1), CoordPoint(0, 2), + CoordPoint(0, 3) }, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("L4", + PiecePoints{ CoordPoint(0, -3), CoordPoint(0, -2), + CoordPoint(0, -1), CoordPoint(0, 0), + CoordPoint(0, 1), CoordPoint(1, 2) }, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("Y", + PiecePoints{ CoordPoint(0, -1), CoordPoint(-1, 0), + CoordPoint(0, 1), CoordPoint(0, 2), + CoordPoint(0, 3)}, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("N", + PiecePoints{ CoordPoint(-2, -1), CoordPoint(-1, 0), + CoordPoint(0, 1), CoordPoint(0, 2), + CoordPoint(0, 3)}, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("V4", + PiecePoints{ CoordPoint(-3, 0), CoordPoint(-2, 0), + CoordPoint(-1, 0), CoordPoint(0, -1), + CoordPoint(0, -2), CoordPoint(0, -3) }, + geo, transforms, piece_set, CoordPoint(-1, 0)); + pieces.emplace_back("W", + PiecePoints{ CoordPoint(-2, -1), CoordPoint(-1, 0), + CoordPoint(0, 1), CoordPoint(1, 2)}, + geo, transforms, piece_set, CoordPoint(-1, 0)); + pieces.emplace_back("Z4", + PiecePoints{ CoordPoint(-1, -2), CoordPoint(0, -1), + CoordPoint(0, 0), CoordPoint(0, 1), + CoordPoint(1, 2) }, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("T4", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(1, 0), + CoordPoint(0, 1), CoordPoint(0, 2), + CoordPoint(0, 3) }, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("E", + PiecePoints{ CoordPoint(0, -1), CoordPoint(1, 0), + CoordPoint(0, 1), CoordPoint(-1, 2)}, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("U4", + PiecePoints{ CoordPoint(-2, -1), CoordPoint(-1, 0), + CoordPoint(0, 0), CoordPoint(1, 0), + CoordPoint(2, -1) }, + geo, transforms, piece_set, CoordPoint(-1, 0)); + pieces.emplace_back("X", + PiecePoints{ CoordPoint(0, -1), CoordPoint(-1, 0), + CoordPoint(1, 0), CoordPoint(0, 1)}, + geo, transforms, piece_set, CoordPoint(0, -1)); + pieces.emplace_back("F", + PiecePoints{ CoordPoint(1, -2), CoordPoint(0, -1), + CoordPoint(1, 0), CoordPoint(0, 1)}, + geo, transforms, piece_set, CoordPoint(0, -1)); + pieces.emplace_back("H", + PiecePoints{ CoordPoint(0, -1), CoordPoint(1, 0), + CoordPoint(0, 1), CoordPoint(2, 1)}, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("J", + PiecePoints{ CoordPoint(0, -3), CoordPoint(0, -2), + CoordPoint(0, -1), CoordPoint(-1, 0), + CoordPoint(-2, -1) }, + geo, transforms, piece_set, CoordPoint(-1, 0)); + pieces.emplace_back("G", + PiecePoints{ CoordPoint(2, -1), CoordPoint(1, 0), + CoordPoint(0, 1), CoordPoint(1, 2)}, + geo, transforms, piece_set, CoordPoint(1, 0)); + pieces.emplace_back("O", + PiecePoints{ CoordPoint(1, 0), CoordPoint(2, 1), + CoordPoint(0, 1), CoordPoint(1, 2)}, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("I3", + PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0), + CoordPoint(0, 1), CoordPoint(0, 2), + CoordPoint(0, 3) }, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("L3", + PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0), + CoordPoint(0, 1), CoordPoint(1, 2) }, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("T3", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(1, 0), + CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("Z3", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 1), + CoordPoint(1, 2) }, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("U3", + PiecePoints{ CoordPoint(0, -1), CoordPoint(1, 0), + CoordPoint(2, -1) }, + geo, transforms, piece_set, CoordPoint(1, 0)); + pieces.emplace_back("V2", + PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, -1) }, + geo, transforms, piece_set, CoordPoint(-1, 0)); + pieces.emplace_back("I2", + PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0), + CoordPoint(0, 1) }, + geo, transforms, piece_set, CoordPoint(0, 1)); + pieces.emplace_back("1", + PiecePoints{ CoordPoint(1, 0) }, + geo, transforms, piece_set, CoordPoint(1, 0)); + return pieces; +} + +} // namespace + +//----------------------------------------------------------------------------- + +BoardConst::BoardConst(BoardType board_type, PieceSet piece_set) + : m_board_type(board_type), + m_piece_set(piece_set), + m_geo(libpentobi_base::get_geometry(board_type)) +{ + switch (board_type) + { + case BoardType::classic: + m_nu_moves = Move::onboard_moves_classic + 1; + break; + case BoardType::trigon: + m_nu_moves = Move::onboard_moves_trigon + 1; + break; + case BoardType::trigon_3: + m_nu_moves = Move::onboard_moves_trigon_3 + 1; + break; + case BoardType::duo: + if (piece_set == PieceSet::classic) + m_nu_moves = Move::onboard_moves_duo + 1; + else + { + LIBBOARDGAME_ASSERT(piece_set == PieceSet::junior); + m_nu_moves = Move::onboard_moves_junior + 1; + } + break; + case BoardType::nexos: + m_nu_moves = Move::onboard_moves_nexos + 1; + break; + case BoardType::callisto: + m_nu_moves = Move::onboard_moves_callisto + 1; + break; + case BoardType::callisto_2: + m_nu_moves = Move::onboard_moves_callisto_2 + 1; + break; + case BoardType::callisto_3: + m_nu_moves = Move::onboard_moves_callisto_3 + 1; + break; + } + switch (piece_set) + { + case PieceSet::classic: + m_transforms.reset(new PieceTransformsClassic); + m_pieces = create_pieces_classic(m_geo, piece_set, *m_transforms); + m_max_piece_size = 5; + m_max_adj_attach = 16; + m_move_info.reset(calloc(m_nu_moves, sizeof(MoveInfo<5>))); + m_move_info_ext.reset(calloc(m_nu_moves, sizeof(MoveInfoExt<16>))); + break; + case PieceSet::junior: + m_transforms.reset(new PieceTransformsClassic); + m_pieces = create_pieces_junior(m_geo, piece_set, *m_transforms); + m_max_piece_size = 5; + m_max_adj_attach = 16; + m_move_info.reset(calloc(m_nu_moves, sizeof(MoveInfo<5>))); + m_move_info_ext.reset(calloc(m_nu_moves, sizeof(MoveInfoExt<16>))); + break; + case PieceSet::trigon: + m_transforms.reset(new PieceTransformsTrigon); + m_pieces = create_pieces_trigon(m_geo, piece_set, *m_transforms); + m_max_piece_size = 6; + m_max_adj_attach = 22; + m_move_info.reset(calloc(m_nu_moves, sizeof(MoveInfo<6>))); + m_move_info_ext.reset(calloc(m_nu_moves, sizeof(MoveInfoExt<22>))); + break; + case PieceSet::nexos: + m_transforms.reset(new PieceTransformsClassic); + m_pieces = create_pieces_nexos(m_geo, piece_set, *m_transforms); + m_max_piece_size = 7; + m_max_adj_attach = 12; + m_move_info.reset(calloc(m_nu_moves, sizeof(MoveInfo<7>))); + m_move_info_ext.reset(calloc(m_nu_moves, sizeof(MoveInfoExt<12>))); + break; + case PieceSet::callisto: + m_transforms.reset(new PieceTransformsClassic); + m_pieces = create_pieces_callisto(m_geo, piece_set, *m_transforms); + m_max_piece_size = 5; + // m_max_adj_attach is actually 10 in Callisto, but we care more about + // the performance in the classic Blokus variants and some code is + // faster if we don't have to handle different values for + // m_max_adj_attach for the same m_max_piece_size. + m_max_adj_attach = 16; + m_move_info.reset(calloc(m_nu_moves, sizeof(MoveInfo<5>))); + m_move_info_ext.reset(calloc(m_nu_moves, sizeof(MoveInfoExt<16>))); + break; + } + m_move_info_ext_2.reset(new MoveInfoExt2[m_nu_moves]); + m_nu_pieces = static_cast(m_pieces.size()); + init_adj_status(); + auto width = m_geo.get_width(); + auto height = m_geo.get_height(); + for (Point p : m_geo) + m_compare_val[p] = + (height - m_geo.get_y(p) - 1) * width + m_geo.get_x(p); + create_moves(); + switch (piece_set) + { + case PieceSet::classic: + LIBBOARDGAME_ASSERT(m_nu_pieces == 21); + break; + case PieceSet::junior: + LIBBOARDGAME_ASSERT(m_nu_pieces == 12); + break; + case PieceSet::trigon: + LIBBOARDGAME_ASSERT(m_nu_pieces == 22); + break; + case PieceSet::nexos: + LIBBOARDGAME_ASSERT(m_nu_pieces == 24); + break; + case PieceSet::callisto: + LIBBOARDGAME_ASSERT(m_nu_pieces == 12); + break; + } + if (board_type == BoardType::duo || board_type == BoardType::callisto_2) + init_symmetry_info<5>(); + else if (board_type == BoardType::trigon) + init_symmetry_info<6>(); +} + +template +inline void BoardConst::create_move(unsigned& moves_created, Piece piece, + const MovePoints& points, Point label_pos) +{ + LIBBOARDGAME_ASSERT(m_max_piece_size == MAX_SIZE); + LIBBOARDGAME_ASSERT(m_max_adj_attach == MAX_ADJ_ATTACH); + LIBBOARDGAME_ASSERT(moves_created < m_nu_moves); + Move mv(static_cast(moves_created)); + void* place = + static_cast*>(m_move_info.get()) + + moves_created; + new(place) MoveInfo(piece, points); + place = + static_cast*>(m_move_info_ext.get()) + + moves_created; + auto& info_ext = *new(place) MoveInfoExt(); + auto& info_ext_2 = m_move_info_ext_2[moves_created]; + ++moves_created; + auto scored_points = &info_ext_2.scored_points[0]; + for (auto p : points) + if (m_board_type != BoardType::nexos || m_geo.get_point_type(p) != 0) + *(scored_points++) = p; + info_ext_2.scored_points_size = static_cast( + scored_points - &info_ext_2.scored_points[0]); + auto begin = info_ext_2.begin_scored_points(); + auto end = info_ext_2.end_scored_points(); + g_marker.clear(); + for (auto i = begin; i != end; ++i) + g_marker.set(*i); + for (auto i = begin; i != end; ++i) + { + auto j = m_adj_status_list[*i].begin(); + unsigned adj_status = g_marker[*j]; + for (unsigned k = 1; k < PrecompMoves::adj_status_nu_adj; ++k) + adj_status |= (g_marker[*(++j)] << k); + for (unsigned j = 0; j < PrecompMoves::nu_adj_status; ++j) + if ((j & adj_status) == 0) + g_full_move_table[*i][j].push_back(mv); + } + Point* p = info_ext.points; + for (auto i = begin; i != end; ++i) + for (Point j : m_geo.get_adj(*i)) + if (! g_marker[j]) + { + g_marker.set(j); + *(p++) = j; + } + info_ext.size_adj_points = static_cast(p - info_ext.points); + for (auto i = begin; i != end; ++i) + for (Point j : m_geo.get_diag(*i)) + if (! g_marker[j]) + { + g_marker.set(j); + *(p++) = j; + } + info_ext.size_attach_points = + static_cast(p - info_ext.end_adj()); + info_ext_2.label_pos = label_pos; + info_ext_2.breaks_symmetry = false; + info_ext_2.symmetric_move = Move::null(); + m_nu_attach_points[piece] = + max(m_nu_attach_points[piece], + static_cast(info_ext.size_attach_points)); + if (log_move_creation) + { + Grid grid; + grid.fill('.', m_geo); + for (auto i = begin; i != end; ++i) + grid[*i] = 'O'; + for (auto i = info_ext.begin_adj(); i != info_ext.end_adj(); ++i) + grid[*i] = '+'; + for (auto i = info_ext.begin_attach(); i != info_ext.end_attach(); ++i) + grid[*i] = '*'; + LIBBOARDGAME_LOG("Move ", mv.to_int(), ":\n", grid.to_string(m_geo)); + } +} + +void BoardConst::create_moves() +{ + // Unused move infos for Move::null() + LIBBOARDGAME_ASSERT(Move::null().to_int() == 0); + unsigned moves_created = 1; + + unsigned n = 0; + for (Piece::IntType i = 0; i < m_nu_pieces; ++i) + { + Piece piece(i); + if (m_max_piece_size == 5) + create_moves<5, 16>(moves_created, piece); + else if (m_max_piece_size == 6) + create_moves<6, 22>(moves_created, piece); + else + create_moves<7, 12>(moves_created, piece); + for (Point p : m_geo) + for (unsigned j = 0; j < PrecompMoves::nu_adj_status; ++j) + { + auto& list = g_full_move_table[p][j]; + m_precomp_moves.set_list_range(p, j, piece, n, + list.size()); + for (auto mv : list) + m_precomp_moves.set_move(n++, mv); + list.clear(); + } + } + LIBBOARDGAME_ASSERT(moves_created == m_nu_moves); + if (log_move_creation) + LIBBOARDGAME_LOG("Created moves: ", moves_created, ", precomp: ", n); +} + +template +void BoardConst::create_moves(unsigned& moves_created, Piece piece) +{ + auto& piece_info = m_pieces[piece.to_int()]; + if (log_move_creation) + LIBBOARDGAME_LOG("Creating moves for piece ", piece_info.get_name()); + auto& transforms = piece_info.get_transforms(); + auto nu_transforms = transforms.size(); + vector transformed_points(nu_transforms); + vector transformed_label_pos(nu_transforms); + for (size_t i = 0; i < nu_transforms; ++i) + { + auto transform = transforms[i]; + transformed_points[i] = piece_info.get_points(); + transform->transform(transformed_points[i].begin(), + transformed_points[i].end()); + sort_piece_points(transformed_points[i]); + transformed_label_pos[i] = + transform->get_transformed(piece_info.get_label_pos()); + } + auto piece_size = + static_cast(piece_info.get_points().size()); + MovePoints points; + for (MovePoints::IntType i = 0; i < MovePoints::max_size; ++i) + points.get_unchecked(i) = Point::null(); + points.resize(piece_size); + // Make outer loop iterator over geometry for better memory locality + for (Point p : m_geo) + { + if (log_move_creation) + LIBBOARDGAME_LOG("Creating moves at ", m_geo.to_string(p)); + auto x = m_geo.get_x(p); + auto y = m_geo.get_y(p); + auto point_type = m_geo.get_point_type(p); + for (size_t i = 0; i < nu_transforms; ++i) + { + if (log_move_creation) + { +#if ! LIBBOARDGAME_DISABLE_LOG + auto& transform = *transforms[i]; + LIBBOARDGAME_LOG("Transformation ", typeid(transform).name()); +#endif + } + if (transforms[i]->get_new_point_type() != point_type) + continue; + bool is_onboard = true; + for (MovePoints::IntType j = 0; j < piece_size; ++j) + { + auto& pp = transformed_points[i][j]; + int xx = pp.x + x; + int yy = pp.y + y; + if (! m_geo.is_onboard(CoordPoint(xx, yy))) + { + is_onboard = false; + break; + } + points[j] = m_geo.get_point(xx, yy); + } + if (! is_onboard) + continue; + CoordPoint label_pos = transformed_label_pos[i]; + label_pos.x += x; + label_pos.y += y; + create_move( + moves_created, piece, points, + m_geo.get_point(label_pos.x, label_pos.y)); + } + } +} + +Move BoardConst::from_string(const string& s) const +{ + string trimmed = to_lower(trim(s)); + if (trimmed == "null") + return Move::null(); + vector v = split(trimmed, ','); + if (v.size() > PieceInfo::max_size) + throw runtime_error("illegal move (too many points)"); + bool is_nexos = (m_board_type == BoardType::nexos); + MovePoints points; + for (const auto& s : v) + { + Point p; + if (! m_geo.from_string(s, p)) + throw runtime_error("illegal move (invalid point)"); + if (is_nexos) + { + auto point_type = m_geo.get_point_type(p); + if (point_type != 1 && point_type != 2) + // Silently discard points that are not line segments, such + // files were written by some (unreleased) versions of Pentobi. + continue; + } + points.push_back(p); + } + Move mv; + if (! find_move(points, mv)) + throw runtime_error("illegal move"); + return mv; +} + +const BoardConst& BoardConst::get(Variant variant) +{ + static map>> board_const; + auto board_type = libpentobi_base::get_board_type(variant); + auto piece_set = libpentobi_base::get_piece_set(variant); + auto& bc = board_const[board_type][piece_set]; + if (! bc) + bc.reset(new BoardConst(board_type, piece_set)); + return *bc; +} + +bool BoardConst::get_piece_by_name(const string& name, Piece& piece) const +{ + for (Piece::IntType i = 0; i < m_nu_pieces; ++i) + if (get_piece_info(Piece(i)).get_name() == name) + { + piece = Piece(i); + return true; + } + return false; +} + +bool BoardConst::find_move(const MovePoints& points, Move& move) const +{ + MovePoints sorted_points = points; + sort(sorted_points); + for (Piece::IntType i = 0; i < m_pieces.size(); ++i) + { + Piece piece(i); + for (auto mv : get_moves(piece, points[0])) + { + auto& info_ext_2 = get_move_info_ext_2(mv); + if (sorted_points.size() == info_ext_2.scored_points_size + && equal(sorted_points.begin(), sorted_points.end(), + info_ext_2.begin_scored_points())) + { + move = mv; + return true; + } + } + } + return false; +} + +bool BoardConst::find_move(const MovePoints& points, Piece piece, + Move& move) const +{ + MovePoints sorted_points = points; + sort(sorted_points); + for (auto mv : get_moves(piece, points[0])) + if (equal(sorted_points.begin(), sorted_points.end(), + get_move_points_begin(mv))) + { + move = mv; + return true; + } + return false; +} + +void BoardConst::init_adj_status() +{ + for (Point p : m_geo) + { + auto& l = m_adj_status_list[p]; + for (Point pp : m_geo.get_adj(p)) + { + if (l.size() == PrecompMoves::adj_status_nu_adj) + break; + l.push_back(pp); + } + for (Point pp : m_geo.get_diag(p)) + { + if (l.size() == PrecompMoves::adj_status_nu_adj) + break; + l.push_back(pp); + } + for (auto i = l.end(); i < l.begin() + PrecompMoves::adj_status_nu_adj; + ++i) + *i = Point::null(); + } +} + +template +void BoardConst::init_symmetry_info() +{ + m_symmetric_points.init(m_geo); + for (Move::IntType i = 1; i < m_nu_moves; ++i) + { + Move mv(i); + auto& info = get_move_info(mv); + auto& info_ext_2 = m_move_info_ext_2[i]; + info_ext_2.breaks_symmetry = false; + array sym_points; + MovePoints::IntType n = 0; + for (Point p : info) + { + auto symm_p = m_symmetric_points[p]; + auto end = info.end(); + if (find(info.begin(), end, symm_p) != end) + info_ext_2.breaks_symmetry = true; + sym_points[n++] = symm_p; + } + for (auto mv : get_moves(info.get_piece(), sym_points[0])) + if (is_reverse(sym_points.begin(), + get_move_info(mv).begin(), n)) + { + info_ext_2.symmetric_move = mv; + break; + } + } +} + +inline void BoardConst::sort(MovePoints& points) const +{ + auto check = [&](unsigned short a, unsigned short b) + { + if (m_compare_val[points[a]] > m_compare_val[points[b]]) + swap(points[a], points[b]); + }; + // Minimal number of necessary comparisons with sorting networks + auto size = points.size(); + switch (size) + { + case 7: + check(1, 2); + check(3, 4); + check(5, 6); + check(0, 2); + check(3, 5); + check(4, 6); + check(0, 1); + check(4, 5); + check(2, 6); + check(0, 4); + check(1, 5); + check(0, 3); + check(2, 5); + check(1, 3); + check(2, 4); + check(2, 3); + break; + case 6: + check(1, 2); + check(4, 5); + check(0, 2); + check(3, 5); + check(0, 1); + check(3, 4); + check(2, 5); + check(0, 3); + check(1, 4); + check(2, 4); + check(1, 3); + check(2, 3); + break; + case 5: + check(0, 1); + check(3, 4); + check(2, 4); + check(2, 3); + check(1, 4); + check(0, 3); + check(0, 2); + check(1, 3); + check(1, 2); + break; + case 4: + check(0, 1); + check(2, 3); + check(0, 2); + check(1, 3); + check(1, 2); + break; + case 3: + check(1, 2); + check(0, 2); + check(0, 1); + break; + case 2: + check(0, 1); + break; + default: + LIBBOARDGAME_ASSERT(size == 1); + } +} + +string BoardConst::to_string(Move mv, bool with_piece_name) const +{ + if (mv.is_null()) + return "null"; + auto& info_ext_2 = get_move_info_ext_2(mv); + ostringstream s; + if (with_piece_name) + s << '[' << get_piece_info(get_move_piece(mv)).get_name() << "]"; + bool is_first = true; + for (auto i = info_ext_2.begin_scored_points(); + i != info_ext_2.end_scored_points(); ++i) + { + if (! is_first) + s << ','; + else + is_first = false; + s << m_geo.to_string(*i); + } + return s.str(); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/BoardConst.h b/src/libpentobi_base/BoardConst.h new file mode 100644 index 0000000..fb15cdb --- /dev/null +++ b/src/libpentobi_base/BoardConst.h @@ -0,0 +1,363 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/BoardConst.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_BOARD_CONST_H +#define LIBPENTOBI_BASE_BOARD_CONST_H + +#include "Geometry.h" +#include "MoveInfo.h" +#include "PieceInfo.h" +#include "PieceTransforms.h" +#include "PrecompMoves.h" +#include "SymmetricPoints.h" +#include "Variant.h" +#include "libboardgame_util/ArrayList.h" +#include "libboardgame_util/Range.h" + +namespace libpentobi_base { + +using namespace std; +using libboardgame_util::ArrayList; +using libboardgame_util::Range; + +//----------------------------------------------------------------------------- + +/** Constant precomputed data that is shared between all instances of Board + with a given board type and set of unique pieces per color. */ +class BoardConst +{ +public: + /** See get_adj_status_list() */ + typedef + ArrayList + AdjStatusList; + + /** Start of the MoveInfo array, which can be cached by the user in + performance-critical code and then passed into the static version of + get_move_info(). */ + typedef const void* MoveInfoArray; + + /** Start of the MoveInfoExt array, which can be cached by the user in + performance-critical code and then passed into the static version of + get_move_info_ext(). */ + typedef const void* MoveInfoExtArray; + + /** Get the single instance for a given board size. + The instance is created the first time this function is called. + This function is not thread-safe. */ + static const BoardConst& get(Variant variant); + + template + static const MoveInfo& + get_move_info(Move mv, MoveInfoArray move_info_array); + + template + static const MoveInfoExt& + get_move_info_ext(Move mv, MoveInfoExtArray move_info_ext_array); + + + Piece::IntType get_nu_pieces() const; + + const PieceInfo& get_piece_info(Piece piece) const; + + unsigned get_nu_attach_points(Piece piece) const; + + bool get_piece_by_name(const string& name, Piece& piece) const; + + const PieceTransforms& get_transforms() const; + + unsigned get_max_piece_size() const { return m_max_piece_size; } + + unsigned get_max_adj_attach() const { return m_max_adj_attach; } + + Range get_move_points(Move mv) const; + + /** Return start of move points array. + For unrolling loops, there are guaranteed to be as many elements + as the maximum piece size in the current game variant. If the piece + is smaller, the remaining points are guaranteed to be Point::null(). */ + const Point* get_move_points_begin(Move mv) const; + + template + const Point* get_move_points_begin(Move mv) const; + + Piece get_move_piece(Move mv) const; + + template + Piece get_move_piece(Move mv) const; + + MoveInfoArray get_move_info_array() const { return m_move_info.get(); } + + /** Get pointer to extended move info array. + Can be used to speed up the access to the move info by avoiding the + multiple pointer dereferencing of Board::get_move_info_ext(Move) */ + MoveInfoExtArray get_move_info_ext_array() const; + + const MoveInfoExt2& get_move_info_ext_2(Move mv) const; + + const MoveInfoExt2* get_move_info_ext_2_array() const; + + unsigned get_nu_moves() const; + + bool find_move(const MovePoints& points, Move& move) const; + + bool find_move(const MovePoints& points, Piece piece, Move& move) const; + + /** Get all moves of a piece at a point constrained by the forbidden + status of adjacent points. */ + PrecompMoves::Range get_moves(Piece piece, Point p, + unsigned adj_status = 0) const + { + return m_precomp_moves.get_moves(piece, p, adj_status); + } + + const PrecompMoves& get_precomp_moves() const { return m_precomp_moves; } + + BoardType get_board_type() const { return m_board_type; }; + + PieceSet get_piece_set() const { return m_piece_set; } + + const Geometry& get_geometry() const; + + /** List containing the points used for the adjacent status. + Contains the first PrecompMoves::adj_status_nu_adj points of + Geometry::get_adj() concatenated with Geometry::get_diag(). + Elements above end() may be accessed and contain Point::null() + for easy unrolling of loops. */ + const AdjStatusList& get_adj_status_list(Point p) const + { + return m_adj_status_list[p]; + } + + /** Only initialized in game variants with central symmetry of board + including startign points. */ + const SymmetricPoints& get_symmetrc_points() const + { + return m_symmetric_points; + } + + /** Convert a move to its string representation. + The string representation is a comma-separated list of points (without + spaces between the commas or points). If with_piece_name is true, + it is prepended by the piece name in square brackets (also without any + spaces). The representation without the piece name is used by the SGF + files and GTP interface used by Pentobi (version >= 0.2). */ + string to_string(Move mv, bool with_piece_name = false) const; + + Move from_string(const string& s) const; + + /** Sort move points using the ordering used in blksgf files. */ + void sort(MovePoints& points) const; + +private: + struct MallocFree + { + void operator()(void* x) { free(x); } + }; + + + Piece::IntType m_nu_pieces; + + unsigned m_nu_moves; + + unsigned m_max_piece_size; + + /** See MoveInfoExt */ + unsigned m_max_adj_attach; + + BoardType m_board_type; + + PieceSet m_piece_set; + + const Geometry& m_geo; + + vector m_pieces; + + Grid m_adj_status_list; + + unique_ptr m_transforms; + + PieceMap m_nu_attach_points{0}; + + /** Array of MoveInfo with MAX_SIZE being the maximum piece size + in the corresponding game variant. + See comments at MoveInfo. */ + unique_ptr m_move_info; + + /** Array of MoveInfoExt with MAX_ADJ_ATTACH being the + maximum total number of attach points and adjacent points of a piece in + the corresponding game variant. + See comments at MoveInfoExt. */ + unique_ptr m_move_info_ext; + + unique_ptr m_move_info_ext_2; + + PrecompMoves m_precomp_moves; + + /** Value for comparing points using the ordering used in blksgf files. + As specified in doc/blksgf/Pentobi-SGF.html, the order should be + (a1, b1, ..., a2, b2, ...) with y going upwards whereas the convention + for Point is that y goes downwards. */ + Grid m_compare_val; + + SymmetricPoints m_symmetric_points; + + + BoardConst(BoardType board_type, PieceSet piece_set); + + template + void create_move(unsigned& moves_created, Piece piece, + const MovePoints& points, Point label_pos); + + void create_moves(); + + template + void create_moves(unsigned& moves_created, Piece piece); + + template + const MoveInfo& get_move_info(Move mv) const; + + void init_adj_status(); + + template + void init_symmetry_info(); +}; + +inline const Geometry& BoardConst::get_geometry() const +{ + return m_geo; +} + +template +inline const MoveInfo& +BoardConst::get_move_info(Move mv, MoveInfoArray move_info_array) +{ + LIBBOARDGAME_ASSERT(! mv.is_null()); + return *(static_cast*>(move_info_array) + + mv.to_int()); +} + +template +inline const MoveInfo& BoardConst::get_move_info(Move mv) const +{ + LIBBOARDGAME_ASSERT(m_max_piece_size == MAX_SIZE); + return get_move_info(mv, m_move_info.get()); +} + +template +inline const MoveInfoExt& +BoardConst::get_move_info_ext(Move mv, MoveInfoExtArray move_info_ext_array) +{ + LIBBOARDGAME_ASSERT(! mv.is_null()); + return *(static_cast*>( + move_info_ext_array) + mv.to_int()); +} + +inline const MoveInfoExt2& BoardConst::get_move_info_ext_2(Move mv) const +{ + LIBBOARDGAME_ASSERT(mv.to_int() < m_nu_moves); + return m_move_info_ext_2[mv.to_int()]; +} + +inline auto BoardConst::get_move_info_ext_array() const -> MoveInfoExtArray +{ + return m_move_info_ext.get(); +} + +inline const MoveInfoExt2* BoardConst::get_move_info_ext_2_array() const +{ + return m_move_info_ext_2.get(); +} + +template +inline Piece BoardConst::get_move_piece(Move mv) const +{ + return get_move_info(mv).get_piece(); +} + +inline Piece BoardConst::get_move_piece(Move mv) const +{ + if (m_max_piece_size == 5) + return get_move_piece<5>(mv); + else if (m_max_piece_size == 6) + return get_move_piece<6>(mv); + else + { + LIBBOARDGAME_ASSERT(m_max_piece_size == 7); + return get_move_piece<7>(mv); + } +} + +inline Range BoardConst::get_move_points(Move mv) const +{ + if (m_max_piece_size == 5) + { + auto& info = get_move_info<5>(mv); + return Range(info.begin(), info.end()); + } + else if (m_max_piece_size == 6) + { + auto& info = get_move_info<6>(mv); + return Range(info.begin(), info.end()); + } + else + { + LIBBOARDGAME_ASSERT(m_max_piece_size == 7); + auto& info = get_move_info<7>(mv); + return Range(info.begin(), info.end()); + } +} + +inline const Point* BoardConst::get_move_points_begin(Move mv) const +{ + if (m_max_piece_size == 5) + return get_move_points_begin<5>(mv); + else if (m_max_piece_size == 6) + return get_move_points_begin<6>(mv); + else + { + LIBBOARDGAME_ASSERT(m_max_piece_size == 7); + return get_move_points_begin<7>(mv); + } +} + +template +inline const Point* BoardConst::get_move_points_begin(Move mv) const +{ + return get_move_info(mv).begin(); +} + +inline unsigned BoardConst::get_nu_moves() const +{ + return m_nu_moves; +} + +inline unsigned BoardConst::get_nu_attach_points(Piece piece) const +{ + return m_nu_attach_points[piece]; +} + +inline Piece::IntType BoardConst::get_nu_pieces() const +{ + return m_nu_pieces; +} + +inline const PieceInfo& BoardConst::get_piece_info(Piece piece) const +{ + LIBBOARDGAME_ASSERT(piece.to_int() < m_pieces.size()); + return m_pieces[piece.to_int()]; +} + +inline const PieceTransforms& BoardConst::get_transforms() const +{ + return *m_transforms; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_BOARD_CONST_H diff --git a/src/libpentobi_base/BoardUpdater.cpp b/src/libpentobi_base/BoardUpdater.cpp new file mode 100644 index 0000000..2a275b6 --- /dev/null +++ b/src/libpentobi_base/BoardUpdater.cpp @@ -0,0 +1,154 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/BoardUpdater.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "BoardUpdater.h" + +#include +#include "BoardUtil.h" +#include "NodeUtil.h" +#include "libboardgame_sgf/SgfUtil.h" + +namespace libpentobi_base { + +using libboardgame_sgf::InvalidTree; +using libboardgame_sgf::util::get_path_from_root; +using libpentobi_base::boardutil::get_current_position_as_setup; + +//----------------------------------------------------------------------------- + +namespace { + +/** List to hold remaining pieces of a color with one entry for each instance + of the same piece. */ +typedef ArrayList +AllPiecesLeftList; + +/** Helper function used in init_setup. */ +void handle_setup_property(const SgfNode& node, const char* id, Color c, + const Board& bd, Setup& setup, + ColorMap& pieces_left) +{ + if (! node.has_property(id)) + return; + vector values = node.get_multi_property(id); + for (const string& s : values) + { + Move mv; + try + { + mv = bd.from_string(s); + } + catch (const runtime_error& e) + { + throw InvalidTree(e.what()); + } + Piece piece = bd.get_move_piece(mv); + if (! pieces_left[c].remove(piece)) + throw InvalidTree("piece played twice"); + setup.placements[c].push_back(mv); + } +} + +/** Helper function used in init_setup. */ +void handle_setup_empty(const SgfNode& node, const Board& bd, Setup& setup, + ColorMap& pieces_left) +{ + if (! node.has_property("AE")) + return; + vector values = node.get_multi_property("AE"); + for (const string& s : values) + { + Move mv; + try + { + mv = bd.from_string(s); + } + catch (const runtime_error& e) + { + throw InvalidTree(e.what()); + } + for (Color c : bd.get_colors()) + { + if (setup.placements[c].remove(mv)) + { + Piece piece = bd.get_move_piece(mv); + LIBBOARDGAME_ASSERT(! pieces_left[c].contains(piece)); + pieces_left[c].push_back(piece); + break; + } + throw InvalidTree("invalid value for AE property"); + } + } +} + +/** Initialize the board with a new setup position. + Class Board only supports setup positions before any moves are played. To + support setup properties in any node, we create a new setup position from + the current position and the setup properties from the node and initialize + the board with it. */ +void init_setup(Board& bd, const SgfNode& node) +{ + Setup setup; + get_current_position_as_setup(bd, setup); + ColorMap all_pieces_left; + for (Color c : bd.get_colors()) + for (Piece piece : bd.get_pieces_left(c)) + for (unsigned i = 0; i < bd.get_nu_piece_instances(piece); ++i) + all_pieces_left[c].push_back(piece); + handle_setup_property(node, "A1", Color(0), bd, setup, all_pieces_left); + handle_setup_property(node, "A2", Color(1), bd, setup, all_pieces_left); + handle_setup_property(node, "A3", Color(2), bd, setup, all_pieces_left); + handle_setup_property(node, "A4", Color(3), bd, setup, all_pieces_left); + // AB, AW are equivalent to A1, A2 but only used in games with two colors + handle_setup_property(node, "AB", Color(0), bd, setup, all_pieces_left); + handle_setup_property(node, "AW", Color(1), bd, setup, all_pieces_left); + handle_setup_empty(node, bd, setup, all_pieces_left); + Color to_play; + if (! libpentobi_base::node_util::get_player(node, setup.to_play)) + { + // Try to guess who should be to play based on the setup pieces. + setup.to_play = Color(0); + for (Color c : bd.get_colors()) + if (setup.placements[c].size() < setup.placements[Color(0)].size()) + { + setup.to_play = c; + break; + } + } + bd.init(&setup); +} + +} // namespace + +//----------------------------------------------------------------------------- + +void BoardUpdater::update(Board& bd, const PentobiTree& tree, + const SgfNode& node) +{ + LIBBOARDGAME_ASSERT(tree.contains(node)); + bd.init(); + get_path_from_root(node, m_path); + for (const auto i : m_path) + { + if (libpentobi_base::node_util::has_setup(*i)) + init_setup(bd, *i); + auto mv = tree.get_move(*i); + if (! mv.is_null()) + { + if (! bd.is_piece_left(mv.color, bd.get_move_piece(mv.move))) + throw InvalidTree("piece played twice"); + bd.play(mv); + } + } +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/BoardUpdater.h b/src/libpentobi_base/BoardUpdater.h new file mode 100644 index 0000000..1925313 --- /dev/null +++ b/src/libpentobi_base/BoardUpdater.h @@ -0,0 +1,36 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/BoardUpdater.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_BOARD_UPDATER_H +#define LIBPENTOBI_BASE_BOARD_UPDATER_H + +#include "Board.h" +#include "PentobiTree.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +/** Updates a board state to a node in a game tree. */ +class BoardUpdater +{ +public: + /** Update the board to a node. + @throws Exception if tree contains invalid properties, moves that play + the same piece twice or other conditions that prevent the updater to + update the board to the given node. */ + void update(Board& bd, const PentobiTree& tree, const SgfNode& node); + +private: + /** Local variable reused for efficiency. */ + vector m_path; +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_BOARD_UPDATER_H diff --git a/src/libpentobi_base/BoardUtil.cpp b/src/libpentobi_base/BoardUtil.cpp new file mode 100644 index 0000000..7e819a6 --- /dev/null +++ b/src/libpentobi_base/BoardUtil.cpp @@ -0,0 +1,91 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/BoardUtil.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "BoardUtil.h" + +#include "PentobiSgfUtil.h" +#if LIBBOARDGAME_DEBUG +#include +#endif + +namespace libpentobi_base { +namespace boardutil { + +using namespace std; +using sgf_util::get_color_id; +using sgf_util::get_setup_id; + +//----------------------------------------------------------------------------- + +#if LIBBOARDGAME_DEBUG +string dump(const Board& bd) +{ + ostringstream s; + auto variant = bd.get_variant(); + Writer writer(s); + writer.begin_tree(); + writer.begin_node(); + writer.write_property("GM", to_string(variant)); + write_setup(writer, variant, bd.get_setup()); + writer.end_node(); + for (unsigned i = 0; i < bd.get_nu_moves(); ++i) + { + writer.begin_node(); + auto mv = bd.get_move(i); + auto id = get_color_id(variant, mv.color); + if (! mv.is_null()) + writer.write_property(id, bd.to_string(mv.move, false)); + writer.end_node(); + } + writer.end_tree(); + return s.str(); +} +#endif + +void get_current_position_as_setup(const Board& bd, Setup& setup) +{ + setup = bd.get_setup(); + for (unsigned i = 0; i < bd.get_nu_moves(); ++i) + { + auto mv = bd.get_move(i); + setup.placements[mv.color].push_back(mv.move); + } + setup.to_play = bd.get_to_play(); +} + +Move get_transformed(const Board& bd, Move mv, + const PointTransform& transform) +{ + auto& geo = bd.get_geometry(); + MovePoints points; + for (auto p : bd.get_move_points(mv)) + points.push_back(transform.get_transformed(p, geo)); + Move transformed_mv; + bd.find_move(points, bd.get_move_piece(mv), transformed_mv); + return transformed_mv; +} + +void write_setup(Writer& writer, Variant variant, const Setup& setup) +{ + auto& board_const = BoardConst::get(variant); + for (Color c : get_colors(variant)) + if (! setup.placements[c].empty()) + { + vector values; + for (Move mv : setup.placements[c]) + values.push_back(board_const.to_string(mv, false)); + writer.write_property(get_setup_id(variant, c), values); + } +} + +//----------------------------------------------------------------------------- + +} // namespace boardutil +} // namespace libpentobi_base diff --git a/src/libpentobi_base/BoardUtil.h b/src/libpentobi_base/BoardUtil.h new file mode 100644 index 0000000..e99717a --- /dev/null +++ b/src/libpentobi_base/BoardUtil.h @@ -0,0 +1,40 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/BoardUtil.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_BOARDUTIL_H +#define LIBPENTOBI_BASE_BOARDUTIL_H + +#include "Board.h" +#include "libboardgame_sgf/Writer.h" + +namespace libpentobi_base { +namespace boardutil { + +using libboardgame_sgf::Writer; + +//----------------------------------------------------------------------------- + +#if LIBBOARDGAME_DEBUG +string dump(const Board& bd); +#endif + +/** Return the current position as setup. + Merges all placements from Board::get_setup() and played moved into a + single setup and sets the setup color to play to the current color to + play. */ +void get_current_position_as_setup(const Board& bd, Setup& setup); + +void write_setup(Writer& writer, Variant variant, const Setup& setup); + +Move get_transformed(const Board& bd, Move mv, + const PointTransform& transform); + +//----------------------------------------------------------------------------- + +} // namespace boardutil +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_BOARDUTIL_H diff --git a/src/libpentobi_base/Book.cpp b/src/libpentobi_base/Book.cpp new file mode 100644 index 0000000..58c9078 --- /dev/null +++ b/src/libpentobi_base/Book.cpp @@ -0,0 +1,144 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Book.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Book.h" + +#include "libboardgame_sgf/TreeReader.h" +#include "libboardgame_util/Log.h" +#include "libpentobi_base/BoardUtil.h" + +//----------------------------------------------------------------------------- + +namespace libpentobi_base { + +using libboardgame_base::PointTransfIdent; +using libboardgame_base::PointTransfRefl; +using libboardgame_base::PointTransfReflRot180; +using libboardgame_base::PointTransfRot180; +using libboardgame_base::PointTransfRot270Refl; +using libboardgame_base::PointTransfTrigonReflRot60; +using libboardgame_base::PointTransfTrigonReflRot120; +using libboardgame_base::PointTransfTrigonReflRot240; +using libboardgame_base::PointTransfTrigonReflRot300; +using libboardgame_base::PointTransfTrigonRot60; +using libboardgame_base::PointTransfTrigonRot120; +using libboardgame_base::PointTransfTrigonRot240; +using libboardgame_base::PointTransfTrigonRot300; +using libboardgame_sgf::InvalidPropertyValue; +using libboardgame_sgf::TreeReader; +using boardutil::get_transformed; + +//----------------------------------------------------------------------------- + +Book::Book(Variant variant) + : m_tree(variant) +{ + get_transforms(variant, m_transforms, m_inv_transforms); +} + +Book::~Book() +{ +} + +Move Book::genmove(const Board& bd, Color c) +{ + if (bd.has_setup()) + // Book cannot handle setup positions + return Move::null(); + Move mv; + for (unsigned i = 0; i < m_transforms.size(); ++i) + if (genmove(bd, c, mv, *m_transforms[i], *m_inv_transforms[i])) + return mv; + return Move::null(); +} + +bool Book::genmove(const Board& bd, Color c, Move& mv, + const PointTransform& transform, + const PointTransform& inv_transform) +{ + LIBBOARDGAME_ASSERT(! bd.has_setup()); + auto node = &m_tree.get_root(); + for (unsigned i = 0; i < bd.get_nu_moves(); ++i) + { + ColorMove color_mv = bd.get_move(i); + color_mv.move = get_transformed(bd, color_mv.move, transform); + node = m_tree.find_child_with_move(*node, color_mv); + if (! node) + return false; + } + node = select_child(bd, c, m_tree, *node, inv_transform); + if (! node) + return false; + mv = get_transformed(bd, m_tree.get_move(*node).move, inv_transform); + return true; +} + +void Book::load(istream& in) +{ + TreeReader reader; + try + { + reader.read(in); + } + catch (const TreeReader::ReadError& e) + { + throw runtime_error(string("could not read book: ") + e.what()); + } + unique_ptr root = reader.get_tree_transfer_ownership(); + m_tree.init(root); + get_transforms(m_tree.get_variant(), m_transforms, m_inv_transforms); +} + +const SgfNode* Book::select_child(const Board& bd, Color c, + const PentobiTree& tree, const SgfNode& node, + const PointTransform& inv_transform) +{ + unsigned nu_children = node.get_nu_children(); + if (nu_children == 0) + return nullptr; + vector good_moves; + for (unsigned i = 0; i < nu_children; ++i) + { + auto& child = node.get_child(i); + ColorMove color_mv = tree.get_move(child); + if (color_mv.is_null()) + { + LIBBOARDGAME_LOG("WARNING: Book contains nodes without moves"); + continue; + } + if (color_mv.color != c) + { + LIBBOARDGAME_LOG("WARNING: Book contains non-alternating move sequences"); + continue; + } + auto mv = get_transformed(bd, color_mv.move, inv_transform); + if (! bd.is_legal(color_mv.color, mv)) + { + LIBBOARDGAME_LOG("WARNING: Book contains illegal move"); + continue; + } + if (m_tree.get_good_move(child) > 0) + { + LIBBOARDGAME_LOG(bd.to_string(mv), " !"); + good_moves.push_back(&child); + } + else + LIBBOARDGAME_LOG(bd.to_string(mv)); + } + if (good_moves.empty()) + return nullptr; + LIBBOARDGAME_LOG("Book moves: ", good_moves.size()); + unsigned nu_good_moves = static_cast(good_moves.size()); + return good_moves[m_random.generate() % nu_good_moves]; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/Book.h b/src/libpentobi_base/Book.h new file mode 100644 index 0000000..0ab77ea --- /dev/null +++ b/src/libpentobi_base/Book.h @@ -0,0 +1,69 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Book.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_BOOK_H +#define LIBPENTOBI_BASE_BOOK_H + +#include +#include "Board.h" +#include "PentobiTree.h" +#include "libboardgame_base/PointTransform.h" +#include "libboardgame_util/RandomGenerator.h" + +namespace libpentobi_base { + +using libboardgame_util::RandomGenerator; + +//----------------------------------------------------------------------------- + +/** Opening book. + Opening books are stored as trees in SGF files. Thay contain move + annotation properties according to the SGF standard. The book will select + randomly among the child nodes that have the move annotation good move + or very good move (TE[1] or TE[2]). */ +class Book +{ +public: + explicit Book(Variant variant); + + ~Book(); + + void load(istream& in); + + Move genmove(const Board& bd, Color c); + + const PentobiTree& get_tree() const; + +private: + typedef libboardgame_base::PointTransform PointTransform; + + PentobiTree m_tree; + + RandomGenerator m_random; + + vector> m_transforms; + + vector> m_inv_transforms; + + bool genmove(const Board& bd, Color c, Move& mv, + const PointTransform& transform, + const PointTransform& inv_transform); + + const SgfNode* select_child(const Board& bd, Color c, + const PentobiTree& tree, const SgfNode& node, + const PointTransform& inv_transform); +}; + +inline const PentobiTree& Book::get_tree() const +{ + return m_tree; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_BOOK_H diff --git a/src/libpentobi_base/CMakeLists.txt b/src/libpentobi_base/CMakeLists.txt new file mode 100644 index 0000000..24ad32f --- /dev/null +++ b/src/libpentobi_base/CMakeLists.txt @@ -0,0 +1,78 @@ +set(pentobi_base_STAT_SRCS + BoardConst.h + BoardConst.cpp + Board.h + Board.cpp + BoardUpdater.h + BoardUpdater.cpp + BoardUtil.h + BoardUtil.cpp + Book.h + Book.cpp + CallistoGeometry.h + CallistoGeometry.cpp + Color.h + Color.cpp + ColorMap.h + ColorMove.h + Game.h + Game.cpp + Geometry.h + Grid.h + Marker.h + Move.h + MoveInfo.h + MoveList.h + MoveMarker.h + MovePoints.h + NexosGeometry.h + NexosGeometry.cpp + NodeUtil.h + NodeUtil.cpp + PentobiSgfUtil.h + PentobiSgfUtil.cpp + PentobiTree.h + PentobiTree.cpp + PentobiTreeWriter.h + PentobiTreeWriter.cpp + Piece.h + PieceInfo.h + PieceInfo.cpp + PieceMap.h + PieceTransformsClassic.h + PieceTransformsClassic.cpp + PieceTransformsClassic.h + PieceTransforms.cpp + PieceTransformsTrigon.h + PieceTransformsTrigon.cpp + PlayerBase.h + PlayerBase.cpp + Point.h + PointList.h + PointState.h + PointState.cpp + PrecompMoves.h + ScoreUtil.h + Setup.h + StartingPoints.h + StartingPoints.cpp + SymmetricPoints.h + SymmetricPoints.cpp + TreeUtil.h + TreeUtil.cpp + TrigonGeometry.h + TrigonGeometry.cpp + TrigonTransform.h + TrigonTransform.cpp + Variant.h + Variant.cpp +) + +if (PENTOBI_BUILD_GTP) + set(pentobi_base_STAT_SRCS ${pentobi_base_STAT_SRCS} + Engine.cpp + Engine.h + ) +endif() + +add_library(pentobi_base STATIC ${pentobi_base_STAT_SRCS}) diff --git a/src/libpentobi_base/CallistoGeometry.cpp b/src/libpentobi_base/CallistoGeometry.cpp new file mode 100644 index 0000000..68dc38c --- /dev/null +++ b/src/libpentobi_base/CallistoGeometry.cpp @@ -0,0 +1,125 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/CallistoGeometry.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "CallistoGeometry.h" + +#include "libboardgame_util/Unused.h" + +namespace libpentobi_base { + +using libboardgame_base::CoordPoint; + +//----------------------------------------------------------------------------- + +namespace { + +unsigned get_size_callisto(unsigned nu_players) +{ + if (nu_players == 2) + return 16; + LIBBOARDGAME_ASSERT(nu_players == 3 || nu_players == 4); + return 20; +} + +unsigned get_edge_callisto(unsigned nu_players) +{ + if (nu_players == 4) + return 6; + LIBBOARDGAME_ASSERT(nu_players == 2 || nu_players == 3); + return 2; +} + +bool is_onboard_callisto(unsigned x, unsigned y, unsigned width, + unsigned height, unsigned edge) +{ + unsigned dy = min(y, height - y - 1); + unsigned min_x = (width - edge) / 2 > dy ? (width - edge) / 2 - dy : 0; + unsigned max_x = width - min_x - 1; + return x >= min_x && x <= max_x; +} + +} // namespace + +//----------------------------------------------------------------------------- + +map> CallistoGeometry::s_geometry; + +CallistoGeometry::CallistoGeometry(unsigned nu_players) +{ + unsigned sz = get_size_callisto(nu_players); + m_edge = get_edge_callisto(nu_players); + Geometry::init(sz, sz); +} + +const CallistoGeometry& CallistoGeometry::get(unsigned nu_players) +{ + auto pos = s_geometry.find(nu_players); + if (pos != s_geometry.end()) + return *pos->second; + shared_ptr geometry(new CallistoGeometry(nu_players)); + return *s_geometry.insert(make_pair(nu_players, geometry)).first->second; +} + +auto CallistoGeometry::get_adj_coord(int x, int y) const -> AdjCoordList +{ + LIBBOARDGAME_UNUSED(x); + LIBBOARDGAME_UNUSED(y); + return AdjCoordList(); +} + +auto CallistoGeometry::get_diag_coord(int x, int y) const -> DiagCoordList +{ + DiagCoordList l; + l.push_back(CoordPoint(x, y - 1)); + l.push_back(CoordPoint(x - 1, y)); + l.push_back(CoordPoint(x + 1, y)); + l.push_back(CoordPoint(x, y + 1)); + return l; +} + +unsigned CallistoGeometry::get_period_x() const +{ + return 1; +} + +unsigned CallistoGeometry::get_period_y() const +{ + return 1; +} + +unsigned CallistoGeometry::get_point_type(int x, int y) const +{ + LIBBOARDGAME_UNUSED(x); + LIBBOARDGAME_UNUSED(y); + return 0; +} + +bool CallistoGeometry::init_is_onboard(unsigned x, unsigned y) const +{ + return is_onboard_callisto(x, y, get_width(), get_height(), m_edge); +} + +bool CallistoGeometry::is_center_section(unsigned x, unsigned y, + unsigned nu_players) +{ + auto size = get_size_callisto(nu_players); + if (x < size / 2 - 3 || y < size / 2 - 3) + return false; + x -= size / 2 - 3; + y -= size / 2 - 3; + if (x > 5 || y > 5) + return false; + return is_onboard_callisto(x, y, 6, 6, 2); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + diff --git a/src/libpentobi_base/CallistoGeometry.h b/src/libpentobi_base/CallistoGeometry.h new file mode 100644 index 0000000..f53cf8d --- /dev/null +++ b/src/libpentobi_base/CallistoGeometry.h @@ -0,0 +1,63 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/CallistoGeometry.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_CALLISTO_GEOMETRY_H +#define LIBPENTOBI_BASE_CALLISTO_GEOMETRY_H + +#include +#include +#include "Geometry.h" + +namespace libpentobi_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Geometry for the board game Callisto. + To fit in with the assumptions of the Blokus engine, points are "diagonal" + to each other if they are actually adjacent on the real board and the + "adjacent" relationship is not used. */ +class CallistoGeometry final + : public Geometry +{ +public: + /** Create or reuse an already created geometry. + @param nu_players The number of players (2, 3, or 4). */ + static const CallistoGeometry& get(unsigned nu_players); + + static bool is_center_section(unsigned x, unsigned y, unsigned nu_players); + + + AdjCoordList get_adj_coord(int x, int y) const override; + + DiagCoordList get_diag_coord(int x, int y) const override; + + unsigned get_point_type(int x, int y) const override; + + unsigned get_period_x() const override; + + unsigned get_period_y() const override; + +protected: + bool init_is_onboard(unsigned x, unsigned y) const override; + +private: + /** Stores already created geometries by number of players. */ + static map> s_geometry; + + + unsigned m_edge; + + + explicit CallistoGeometry(unsigned nu_players); +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_CALLISTO_GEOMETRY_H diff --git a/src/libpentobi_base/Color.cpp b/src/libpentobi_base/Color.cpp new file mode 100644 index 0000000..e5869a9 --- /dev/null +++ b/src/libpentobi_base/Color.cpp @@ -0,0 +1,72 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Color.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Color.h" + +#include +#include "libboardgame_util/StringUtil.h" + +namespace libpentobi_base { + +using libboardgame_util::to_lower; + +//----------------------------------------------------------------------------- + +Color::Color(const string& s) +{ + istringstream in(s); + in >> *this; + if (! in) + throw InvalidString("Invalid color string '" + s + "'"); +} + +//----------------------------------------------------------------------------- + +ostream& operator<<(ostream& out, const Color& c) +{ + out << (c.to_int() + 1); + return out; +} + +istream& operator>>(istream& in, Color& c) +{ + string s; + in >> s; + if (in) + { + s = to_lower(s); + if (s == "1" || s == "b" || s == "black") + { + c = Color(0); + return in; + } + else if (s == "2" || s == "w" || s == "white") + { + c = Color(1); + return in; + } + else if (s == "3") + { + c = Color(2); + return in; + } + else if (s == "4") + { + c = Color(3); + return in; + } + } + in.setstate(ios::failbit); + return in; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/Color.h b/src/libpentobi_base/Color.h new file mode 100644 index 0000000..3d7a0da --- /dev/null +++ b/src/libpentobi_base/Color.h @@ -0,0 +1,190 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Color.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_COLOR_H +#define LIBPENTOBI_BASE_COLOR_H + +#include +#include +#include +#include +#include "libboardgame_util/Assert.h" + +namespace libpentobi_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +class Color +{ +public: + typedef uint_fast8_t IntType; + + class InvalidString + : public runtime_error + { + using runtime_error::runtime_error; + }; + + class Iterator + { + public: + explicit Iterator(IntType i) + { + m_i = i; + } + + bool operator==(Iterator it) const + { + return m_i == it.m_i; + } + + bool operator!=(Iterator it) const + { + return m_i != it.m_i; + } + + void operator++() + { + ++m_i; + } + + Color operator*() const + { + return Color(m_i); + } + + private: + IntType m_i; + }; + + class Range + { + public: + explicit Range(IntType nu_colors) + : m_nu_colors(nu_colors) + { } + + Iterator begin() const { return Iterator(0); } + + Iterator end() const { return Iterator(m_nu_colors); } + + private: + IntType m_nu_colors; + }; + + static const IntType range = 4; + + Color(); + + explicit Color(IntType i); + + explicit Color(const string& s); + + bool operator==(const Color& c) const; + + bool operator!=(const Color& c) const; + + bool operator<(const Color& c) const; + + IntType to_int() const; + + Color get_next(IntType nu_colors) const; + + Color get_previous(IntType nu_colors) const; + +private: + static const IntType value_uninitialized = range; + + IntType m_i; + + bool is_initialized() const; +}; + + +inline Color::Color() +{ +#if LIBBOARDGAME_DEBUG + m_i = value_uninitialized; +#endif +} + +inline Color::Color(IntType i) +{ + LIBBOARDGAME_ASSERT(i < range); + m_i = i; +} + +inline bool Color::operator==(const Color& c) const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + LIBBOARDGAME_ASSERT(c.is_initialized()); + return m_i == c.m_i; +} + +inline bool Color::operator!=(const Color& c) const +{ + return ! operator==(c); +} + +inline bool Color::operator<(const Color& c) const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + LIBBOARDGAME_ASSERT(c.is_initialized()); + return m_i < c.m_i; +} + +inline Color Color::get_next(IntType nu_colors) const +{ + return Color(static_cast(m_i + 1) % nu_colors); +} + +inline Color Color::get_previous(IntType nu_colors) const +{ + return Color(static_cast(m_i + nu_colors - 1) % nu_colors); +} + +inline bool Color::is_initialized() const +{ + return m_i < value_uninitialized; +} + +inline Color::IntType Color::to_int() const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + return m_i; +} + +//----------------------------------------------------------------------------- + +/** Output string representation of color. + The strings "1", "2", ... are used for the colors. */ +ostream& operator<<(ostream& out, const Color& c); + +/** Read color from input stream. + Accepts the strings "1", "2", ..., as well as "b", "w" or "black", "white" + for the first two colors. */ +istream& operator>>(istream& in, Color& c); + +//----------------------------------------------------------------------------- + +/** Unrolled loop over all colors. */ +template +inline void for_each_color(FUNCTION f) +{ + static_assert(Color::range == 4, ""); + f(Color(0)); + f(Color(1)); + f(Color(2)); + f(Color(3)); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_COLOR_H diff --git a/src/libpentobi_base/ColorMap.h b/src/libpentobi_base/ColorMap.h new file mode 100644 index 0000000..66b3610 --- /dev/null +++ b/src/libpentobi_base/ColorMap.h @@ -0,0 +1,69 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/ColorMap.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_COLOR_MAP_H +#define LIBPENTOBI_BASE_COLOR_MAP_H + +#include +#include "Color.h" + +namespace libpentobi_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Container mapping a color to another element type. + The elements must be default-constructible. This requirement is due to the + fact that elements are stored in an array for efficient access by color + index and arrays need default-constructible elements. */ +template +class ColorMap +{ +public: + ColorMap() = default; + + explicit ColorMap(const T& val); + + T& operator[](Color c); + + const T& operator[](Color c) const; + + void fill(const T& val); + +private: + array m_a; +}; + +template +inline ColorMap::ColorMap(const T& val) +{ + fill(val); +} + +template +inline T& ColorMap::operator[](Color c) +{ + return m_a[c.to_int()]; +} + +template +inline const T& ColorMap::operator[](Color c) const +{ + return m_a[c.to_int()]; +} + +template +void ColorMap::fill(const T& val) +{ + m_a.fill(val); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_COLOR_MAP_H diff --git a/src/libpentobi_base/ColorMove.h b/src/libpentobi_base/ColorMove.h new file mode 100644 index 0000000..3bbe2e8 --- /dev/null +++ b/src/libpentobi_base/ColorMove.h @@ -0,0 +1,80 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/ColorMove.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_COLOR_MOVE_H +#define LIBPENTOBI_BASE_COLOR_MOVE_H + +#include "Move.h" +#include "libpentobi_base/Color.h" + +namespace libpentobi_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +struct ColorMove +{ + Color color; + + Move move; + + /** Return a color move with a null move and an undefined color. + Even if the color is logically not defined, it is still initialized + (with Color(0)), such that this color move can be used in + comparisons. If you are sure that the color is never used and don't + want to initialize it for efficiency, use the default constructor + and then assign only the move. */ + static ColorMove null(); + + ColorMove() = default; + + ColorMove(Color c, Move mv); + + /** Equality operator. + @pre move, color, mv.move, mv.color are initialized. */ + bool operator==(const ColorMove& mv) const; + + /** Inequality operator. + @pre move, color, mv.move, mv.color are initialized. */ + bool operator!=(const ColorMove& mv) const; + + bool is_null() const; +}; + +inline ColorMove::ColorMove(Color c, Move mv) + : color(c), + move(mv) +{ +} + +inline bool ColorMove::operator==(const ColorMove& mv) const +{ + return move == mv.move && color == mv.color; +} + +inline bool ColorMove::operator!=(const ColorMove& mv) const +{ + return ! operator==(mv); +} + +inline bool ColorMove::is_null() const +{ + return move.is_null(); +} + +inline ColorMove ColorMove::null() +{ + return ColorMove(Color(0), Move::null()); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_BASE_COLOR_MOVE_H diff --git a/src/libpentobi_base/Engine.cpp b/src/libpentobi_base/Engine.cpp new file mode 100644 index 0000000..2b49931 --- /dev/null +++ b/src/libpentobi_base/Engine.cpp @@ -0,0 +1,379 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Engine.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Engine.h" + +#include +#include "MoveMarker.h" +#include "PentobiTreeWriter.h" +#include "libboardgame_sgf/TreeReader.h" +#include "libboardgame_sgf/SgfUtil.h" +#include "libboardgame_util/Log.h" +#include "libboardgame_util/RandomGenerator.h" + +namespace libpentobi_base { + +using libboardgame_gtp::Failure; +using libboardgame_sgf::InvalidPropertyValue; +using libboardgame_sgf::TreeReader; +using libboardgame_sgf::util::get_last_node; +using libboardgame_util::ArrayList; +using libboardgame_util::RandomGenerator; + +//----------------------------------------------------------------------------- + +Engine::Engine(Variant variant) + : m_game(variant) +{ + add("all_legal", &Engine::cmd_all_legal); + add("clear_board", &Engine::cmd_clear_board); + add("final_score", &Engine::cmd_final_score); + add("get_place", &Engine::cmd_get_place); + add("loadsgf", &Engine::cmd_loadsgf); + add("point_integers", &Engine::cmd_point_integers); + add("move_info", &Engine::cmd_move_info); + add("p", &Engine::cmd_p); + add("param_base", &Engine::cmd_param_base); + add("play", &Engine::cmd_play); + add("savesgf", &Engine::cmd_savesgf); + add("set_game", &Engine::cmd_set_game); + add("showboard", &Engine::cmd_showboard); + add("undo", &Engine::cmd_undo); +} + +void Engine::board_changed() +{ + if (m_show_board) + LIBBOARDGAME_LOG(get_board()); +} + +void Engine::cmd_all_legal(const Arguments& args, Response& response) +{ + auto& bd = get_board(); + unique_ptr moves(new MoveList); + unique_ptr marker(new MoveMarker); + bd.gen_moves(get_color_arg(args), *marker, *moves); + for (Move mv : *moves) + response << bd.to_string(mv, false) << '\n'; +} + +void Engine::cmd_clear_board() +{ + m_game.init(); + board_changed(); +} + +void Engine::cmd_final_score(Response& response) +{ + auto& bd = get_board(); + if (get_nu_players(bd.get_variant()) > 2) + { + for (Color c : bd.get_colors()) + response << bd.get_points(c) << ' '; + } + else + { + auto score = bd.get_score_twoplayer(Color(0)); + if (score > 0) + response << "B+" << score; + else if (score < 0) + response << "W+" << (-score); + else + response << "0"; + } +} + +void Engine::cmd_g(Response& response) +{ + genmove(get_board().get_effective_to_play(), response); +} + +void Engine::cmd_genmove(const Arguments& args, Response& response) +{ + genmove(get_color_arg(args), response); +} + +void Engine::cmd_get_place(const Arguments& args, Response& response) +{ + auto& bd = get_board(); + unsigned place; + bool isPlaceShared; + bd.get_place(get_color_arg(args), place, isPlaceShared); + response << place; + if (isPlaceShared) + response << " shared"; +} + +void Engine::cmd_loadsgf(const Arguments& args) +{ + args.check_size_less_equal(2); + string file = args.get(0); + int move_number = -1; + if (args.get_size() == 2) + move_number = args.parse_min(1, 1) - 1; + try + { + TreeReader reader; + reader.read(file); + auto tree = reader.get_tree_transfer_ownership(); + m_game.init(tree); + const SgfNode* node = nullptr; + if (move_number != -1) + node = m_game.get_tree().get_node_before_move_number(move_number); + if (! node) + node = &get_last_node(m_game.get_root()); + m_game.goto_node(*node); + board_changed(); + } + catch (const runtime_error& e) + { + throw Failure(e.what()); + } +} + +/** Return move info of a move given by its integer or string representation. */ +void Engine::cmd_move_info(const Arguments& args, Response& response) +{ + auto& bd = get_board(); + Move mv; + try + { + mv = Move(args.parse()); + } + catch (const Failure&) + { + try + { + mv = bd.from_string(args.get()); + } + catch (const runtime_error&) + { + ostringstream msg; + msg << "invalid argument '" << args.get() + << "' (expected move or move ID)"; + throw Failure(msg.str()); + } + } + auto& geo = bd.get_geometry(); + Piece piece = bd.get_move_piece(mv); + auto& info_ext_2 = bd.get_move_info_ext_2(mv); + response + << "\n" + << "ID: " << mv.to_int() << "\n" + << "Piece: " << static_cast(piece.to_int()) + << " (" << bd.get_piece_info(piece).get_name() << ")\n" + << "Points:"; + for (Point p : bd.get_move_points(mv)) + response << ' ' << geo.to_string(p); + response + << "\n" + << "BrkSym: " << info_ext_2.breaks_symmetry << "\n" + << "SymMv: " << bd.to_string(info_ext_2.symmetric_move); +} + +void Engine::cmd_p(const Arguments& args) +{ + play(get_board().get_to_play(), args, 0); +} + +void Engine::cmd_param_base(const Arguments& args, Response& response) +{ + if (args.get_size() == 0) + response + << "accept_illegal " << m_accept_illegal << '\n' + << "resign " << m_resign << '\n'; + else + { + args.check_size(2); + string name = args.get(0); + if (name == "accept_illegal") + m_accept_illegal = args.parse(1); + else if (name == "resign") + m_resign = args.parse(1); + else + { + ostringstream msg; + msg << "unknown parameter '" << name << "'"; + throw Failure(msg.str()); + } + } +} + +void Engine::cmd_play(const Arguments& args) +{ + play(get_color_arg(args, 0), args, 1); +} + +void Engine::cmd_point_integers(Response& response) +{ + auto& geo = get_board().get_geometry(); + Grid grid; + for (Point p : geo) + grid[p] = p.to_int(); + response << '\n' << grid.to_string(geo); +} + +void Engine::cmd_reg_genmove(const Arguments& args, Response& response) +{ + RandomGenerator::set_global_seed_last(); + Move move = get_player().genmove(get_board(), get_color_arg(args)); + if (move.is_null()) + throw Failure("player failed to generate a move"); + response << get_board().to_string(move, false); +} + +void Engine::cmd_savesgf(const Arguments& args) +{ + ofstream out(args.get()); + PentobiTreeWriter writer(out, m_game.get_tree()); + writer.set_indent(1); + writer.write(); + if (! out) + throw Failure(strerror(errno)); +} + +/** Set the game variant. + Argument: game variant as in GM property of Pentobi SGF files +
+ This command is similar to the command that is used by Quarry + (http://home.gna.org/quarry/) to set a game at GTP engines that support + multiple games. */ +void Engine::cmd_set_game(const Arguments& args) +{ + Variant variant; + if (! parse_variant(args.get_line(), variant)) + throw Failure("invalid argument"); + m_game.init(variant); + board_changed(); +} + +void Engine::cmd_showboard(Response& response) +{ + response << '\n' << get_board(); +} + +void Engine::cmd_undo() +{ + auto& bd = get_board(); + if (bd.get_nu_moves() == 0) + throw Failure("cannot undo"); + m_game.undo(); + board_changed(); +} + +void Engine::genmove(Color c, Response& response) +{ + auto& bd = get_board(); + auto& player = get_player(); + auto mv = player.genmove(bd, c); + if (mv.is_null()) + { + response << "pass"; + return; + } + if (! bd.is_legal(c, mv)) + { + ostringstream msg; + msg << "player generated illegal move: " << bd.to_string(mv); + throw Failure(msg.str()); + } + if (m_resign && player.resign()) + { + response << "resign"; + return; + } + m_game.play(c, mv, true); + response << bd.to_string(mv, false); + board_changed(); +} + +Color Engine::get_color_arg(const Arguments& args) const +{ + if (args.get_size() > 1) + throw Failure("too many arguments"); + return get_color_arg(args, 0); +} + +Color Engine::get_color_arg(const Arguments& args, unsigned i) const +{ + string s = args.get_tolower(i); + auto& bd = get_board(); + auto variant = bd.get_variant(); + if (get_nu_colors(variant) == 2) + { + if (s == "blue" || s == "black" || s == "b") + return Color(0); + if (s == "green" || s == "white" || s == "w") + return Color(1); + } + else + { + if (s == "1" || s == "blue") + return Color(0); + if (s == "2" || s == "yellow") + return Color(1); + if (s == "3" || s == "red") + return Color(2); + if (s == "4" || s == "green") + return Color(3); + } + throw Failure("invalid color argument '" + s + "'"); +} + +PlayerBase& Engine::get_player() const +{ + if (! m_player) + throw Failure("no player set"); + return *m_player; +} + +void Engine::play(Color c, const Arguments& args, unsigned arg_move_begin) +{ + auto& bd = get_board(); + if (bd.get_nu_moves() >= Board::max_game_moves) + throw Failure("too many moves"); + Move mv; + try + { + if (arg_move_begin == 0) + mv = bd.from_string(args.get_line()); + else + mv = bd.from_string(args.get_remaining_line(arg_move_begin - 1)); + } + catch (const runtime_error& e) + { + throw Failure(e.what()); + } + if (mv.is_null()) + throw Failure("play pass not supported (anymore)"); + if (! m_accept_illegal && ! bd.is_legal(c, mv)) + throw Failure("illegal move"); + m_game.play(c, mv, true); + board_changed(); +} + +void Engine::set_player(PlayerBase& player) +{ + m_player = &player; + add("genmove", &Engine::cmd_genmove); + add("g", &Engine::cmd_g); + add("reg_genmove", &Engine::cmd_reg_genmove); +} + +void Engine::set_show_board(bool enable) +{ + if (enable && ! m_show_board) + LIBBOARDGAME_LOG(get_board()); + m_show_board = enable; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/Engine.h b/src/libpentobi_base/Engine.h new file mode 100644 index 0000000..d4e1493 --- /dev/null +++ b/src/libpentobi_base/Engine.h @@ -0,0 +1,104 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Engine.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_ENGINE_H +#define LIBPENTOBI_BASE_ENGINE_H + +#include "libpentobi_base/Game.h" +#include "libpentobi_base/PlayerBase.h" +#include "libboardgame_base/Engine.h" + +namespace libpentobi_base { + +using namespace std; +using libboardgame_gtp::Arguments; +using libboardgame_gtp::Response; + +//----------------------------------------------------------------------------- + +/** GTP Blokus engine. */ +class Engine + : public libboardgame_base::Engine +{ +public: + explicit Engine(Variant variant); + + void cmd_all_legal(const Arguments&, Response&); + void cmd_clear_board(); + void cmd_final_score(Response&); + void cmd_g(Response&); + void cmd_genmove(const Arguments&, Response&); + void cmd_get_place(const Arguments& args, Response&); + void cmd_loadsgf(const Arguments&); + void cmd_move_info(const Arguments&, Response&); + void cmd_p(const Arguments&); + void cmd_param_base(const Arguments&, Response&); + void cmd_play(const Arguments&); + void cmd_point_integers(Response&); + void cmd_showboard(Response&); + void cmd_reg_genmove(const Arguments&, Response&); + void cmd_savesgf(const Arguments&); + void cmd_set_game(const Arguments&); + void cmd_undo(); + + /** Set the player. + @param player The player (@ref libboardgame_doc_storesref) */ + void set_player(PlayerBase& player); + + void set_accept_illegal(bool enable); + + /** Enable or disable resigning. */ + void set_resign(bool enable); + + void set_show_board(bool enable); + + const Board& get_board() const; + +protected: + Color get_color_arg(const Arguments& args, unsigned i) const; + + Color get_color_arg(const Arguments& args) const; + +private: + bool m_accept_illegal = false; + + bool m_show_board = false; + + bool m_resign = true; + + Game m_game; + + PlayerBase* m_player = nullptr; + + void board_changed(); + + void genmove(Color c, Response& response); + + PlayerBase& get_player() const; + + void play(Color c, const Arguments& args, unsigned arg_move_begin); +}; + +inline const Board& Engine::get_board() const +{ + return m_game.get_board(); +} + +inline void Engine::set_accept_illegal(bool enable) +{ + m_accept_illegal = enable; +} + +inline void Engine::set_resign(bool enable) +{ + m_resign = enable; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_ENGINE_H diff --git a/src/libpentobi_base/Game.cpp b/src/libpentobi_base/Game.cpp new file mode 100644 index 0000000..ce39236 --- /dev/null +++ b/src/libpentobi_base/Game.cpp @@ -0,0 +1,191 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Game.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Game.h" + +#include "BoardUtil.h" +#include "libboardgame_sgf/InvalidTree.h" +#include "libboardgame_sgf/SgfUtil.h" + +namespace libpentobi_base { + +using libboardgame_sgf::InvalidTree; +using libboardgame_sgf::util::back_to_main_variation; +using libboardgame_sgf::util::is_main_variation; +using libpentobi_base::boardutil::get_current_position_as_setup; + +//----------------------------------------------------------------------------- + +Game::Game(Variant variant) + : m_bd(new Board(variant)), + m_tree(variant) +{ + init(variant); +} + +Game::~Game() = default; + +void Game::add_setup(Color c, Move mv) +{ + auto& node = m_tree.add_setup(*m_current, c, mv); + goto_node(node); +} + +void Game::delete_all_variations() +{ + goto_node(back_to_main_variation(*m_current)); + m_tree.delete_all_variations(); +} + +Color Game::get_to_play_default(const Game& game) +{ + auto& tree = game.get_tree(); + auto& bd = game.get_board(); + auto node = &game.get_current(); + Color next = Color(0); + while (node) + { + auto mv = tree.get_move(*node); + if (! mv.is_null()) + { + next = bd.get_next(mv.color); + break; + } + Color c; + if (libpentobi_base::node_util::get_player(*node, c)) + return c; + node = node->get_parent_or_null(); + } + return bd.get_effective_to_play(next); +} + +void Game::goto_node(const SgfNode& node) +{ + auto old = m_current; + try + { + update(node); + } + catch (const InvalidTree&) + { + // Try to restore the old state. + if (! old) + m_current = &node; + else + { + try + { + update(*old); + } + catch (const InvalidTree&) + { + } + } + throw; + } +} + +void Game::init(Variant variant) +{ + m_bd->init(variant); + m_tree.init_variant(variant); + m_current = &m_tree.get_root(); +} + +void Game::init(unique_ptr& root) +{ + m_tree.init(root); + m_bd->init(m_tree.get_variant()); + m_current = nullptr; + goto_node(m_tree.get_root()); +} + +void Game::keep_only_position() +{ + m_tree.keep_only_subtree(*m_current); + m_tree.remove_children(m_tree.get_root()); + m_current = nullptr; + goto_node(m_tree.get_root()); +} + +void Game::keep_only_subtree() +{ + m_tree.keep_only_subtree(*m_current); + m_current = nullptr; + goto_node(m_tree.get_root()); +} + +void Game::play(ColorMove mv, bool always_create_new_node) +{ + m_bd->play(mv); + const SgfNode* child = nullptr; + if (! always_create_new_node) + child = m_tree.find_child_with_move(*m_current, mv); + if (child) + m_current = child; + else + { + m_current = &m_tree.create_new_child(*m_current); + m_tree.set_move(*m_current, mv); + } + set_to_play(get_to_play_default(*this)); +} + +void Game::remove_player() +{ + m_tree.remove_player(*m_current); + update(*m_current); +} + +void Game::remove_setup(Color c, Move mv) +{ + auto& node = m_tree.remove_setup(*m_current, c, mv); + goto_node(node); +} + +void Game::set_player(Color c) +{ + m_tree.set_player(*m_current, c); + update(*m_current); +} + +void Game::set_result(int score) +{ + if (is_main_variation(*m_current)) + m_tree.set_result(m_tree.get_root(), score); +} + +void Game::set_to_play(Color c) +{ + m_bd->set_to_play(c); +} + +void Game::truncate() +{ + goto_node(m_tree.truncate(*m_current)); +} + +void Game::undo() +{ + LIBBOARDGAME_ASSERT(! m_tree.get_move(*m_current).is_null()); + LIBBOARDGAME_ASSERT(m_current->has_parent()); + truncate(); +} + +void Game::update(const SgfNode& node) +{ + m_updater.update(*m_bd, m_tree, node); + m_current = &node; + set_to_play(get_to_play_default(*this)); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/Game.h b/src/libpentobi_base/Game.h new file mode 100644 index 0000000..0f5fa5c --- /dev/null +++ b/src/libpentobi_base/Game.h @@ -0,0 +1,429 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Game.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_GAME_H +#define LIBPENTOBI_BASE_GAME_H + +#include "Board.h" +#include "BoardUpdater.h" +#include "NodeUtil.h" +#include "PentobiTree.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +class Game +{ +public: + /** Determine a sensible value for the color to play at the current node. + If the color was explicitely set with a setup property, it will be + used. Otherwise, the effective color to play will be used, starting + with the next color of the color of the last move (see + Board::get_effective_to_play(Color)) */ + static Color get_to_play_default(const Game& game); + + + explicit Game(Variant variant); + + ~Game(); + + + void init(Variant variant); + + void init(); + + /** Initialize game from a SGF tree. + @note If the tree contains invalid properties, future calls to + goto_node() might throw an exception. + @param root The root node of the SGF tree; the ownership is transferred + to this class. + @throws InvalidTree, if the root node contains invalid + properties */ + void init(unique_ptr& root); + + const Board& get_board() const; + + Variant get_variant() const; + + const SgfNode& get_current() const; + + const SgfNode& get_root() const; + + const PentobiTree& get_tree() const; + + /** Get the current color to play. + Initialized with get_to_play_default() but may be changed with + set_to_play(). */ + Color get_to_play() const; + + /** @param mv + @param always_create_new_node Always create a new child of the current + node even if a child with the move already exists. */ + void play(ColorMove mv, bool always_create_new_node); + + void play(Color c, Move mv, bool always_create_new_node); + + /** Update game state to a node in the tree. + @throws InvalidTree, if the game was constructed with an + external SGF tree and the tree contained invalid property values + (syntactically or sematically, like moves on occupied points). If an + exception is thrown, the current node is not changed. */ + void goto_node(const SgfNode& node); + + /** Undo the current move and go to parent node. + @pre ! get_current().get_move().is_null() + @pre get_current()->has_parent() + @note Even if the implementation of this function calls goto_node(), + it cannot throw an InvalidPropertyValue because the class Game ensures + that the current node is always reachable via a path of nodes with + valid move properties. */ + void undo(); + + /** Set the current color to play. + Does not store a player property in the tree or affect what color is to + play when navigating away from and back to the current node. */ + void set_to_play(Color c); + + ColorMove get_move() const; + + /** See libpentobi_base::Tree::get_move_ignore_invalid() */ + ColorMove get_move_ignore_invalid() const; + + /** Add final score to root node if the current node is in the main + variation. */ + void set_result(int score); + + void set_charset(const string& charset); + + void remove_move_annotation(); + + double get_bad_move() const; + + double get_good_move() const; + + bool is_doubtful_move() const; + + bool is_interesting_move() const; + + void set_bad_move(double value = 1); + + void set_good_move(double value = 1); + + void set_doubtful_move(); + + void set_interesting_move(); + + string get_comment() const; + + void set_comment(const string& s); + + /** Delete the current node and its subtree and go to the parent node. + @pre get_current().has_parent() */ + void truncate(); + + void truncate_children(); + + /** Replace the game tree by a new one that has the current position + as a setup in its root node. */ + void keep_only_position(); + + /** Like keep_only_position() but does not delete the children of the + current node. */ + void keep_only_subtree(); + + void make_main_variation(); + + void move_up_variation(); + + void move_down_variation(); + + /** Delete all variations but the main variation. + If the current node is not in the main variation it will be changed + to the node as in libboardgame_sgf::util::back_to_main_variation() */ + void delete_all_variations(); + + /** Make the current node the first child of its parent. */ + void make_first_child(); + + void set_modified(); + + void clear_modified(); + + bool is_modified() const; + + /** Set the AP property at the root node. */ + void set_application(const string& name, const string& version = ""); + + string get_player_name(Color c) const; + + void set_player_name(Color c, const string& name); + + string get_date() const; + + void set_date(const string& date); + + void set_date_today(); + + /** Get event info (standard property EV) from root node. */ + string get_event() const; + + void set_event(const string& event); + + /** Get round info (standard property RO) from root node. */ + string get_round() const; + + void set_round(const string& round); + + /** Get time info (standard property TM) from root node. */ + string get_time() const; + + void set_time(const string& time); + + bool has_setup() const; + + void add_setup(Color c, Move mv); + + void remove_setup(Color c, Move mv); + + /** See libpentobi_base::Tree::set_player() */ + void set_player(Color c); + + /** See libpentobi_base::Tree::remove_player() */ + void remove_player(); + +private: + const SgfNode* m_current; + + unique_ptr m_bd; + + PentobiTree m_tree; + + BoardUpdater m_updater; + + void update(const SgfNode& node); +}; + +inline void Game::clear_modified() +{ + m_tree.clear_modified(); +} + +inline double Game::get_bad_move() const +{ + return m_tree.get_bad_move(*m_current); +} + +inline const Board& Game::get_board() const +{ + return *m_bd; +} + +inline string Game::get_comment() const +{ + return m_tree.get_comment(*m_current); +} + +inline string Game::get_date() const +{ + return m_tree.get_date(); +} + +inline string Game::get_event() const +{ + return m_tree.get_event(); +} + +inline const SgfNode& Game::get_current() const +{ + return *m_current; +} + +inline double Game::get_good_move() const +{ + return m_tree.get_good_move(*m_current); +} + +inline ColorMove Game::get_move() const +{ + return m_tree.get_move(*m_current); +} + +inline ColorMove Game::get_move_ignore_invalid() const +{ + return m_tree.get_move_ignore_invalid(*m_current); +} + +inline string Game::get_player_name(Color c) const +{ + return m_tree.get_player_name(c); +} + +inline Color Game::get_to_play() const +{ + return m_bd->get_to_play(); +} + +inline string Game::get_round() const +{ + return m_tree.get_round(); +} + +inline const SgfNode& Game::get_root() const +{ + return m_tree.get_root(); +} + +inline string Game::get_time() const +{ + return m_tree.get_time(); +} + +inline const PentobiTree& Game::get_tree() const +{ + return m_tree; +} + +inline bool Game::has_setup() const +{ + return libpentobi_base::node_util::has_setup(*m_current); +} + +inline Variant Game::get_variant() const +{ + return m_bd->get_variant(); +} + +inline void Game::init() +{ + init(m_bd->get_variant()); +} + +inline bool Game::is_doubtful_move() const +{ + return m_tree.is_doubtful_move(*m_current); +} + +inline bool Game::is_interesting_move() const +{ + return m_tree.is_interesting_move(*m_current); +} + +inline bool Game::is_modified() const +{ + return m_tree.is_modified(); +} + +inline void Game::make_first_child() +{ + m_tree.make_first_child(*m_current); +} + +inline void Game::make_main_variation() +{ + m_tree.make_main_variation(*m_current); +} + +inline void Game::move_down_variation() +{ + m_tree.move_down(*m_current); +} + +inline void Game::move_up_variation() +{ + m_tree.move_up(*m_current); +} + +inline void Game::play(Color c, Move mv, bool always_create_new_node) +{ + play(ColorMove(c, mv), always_create_new_node); +} + +inline void Game::remove_move_annotation() +{ + m_tree.remove_move_annotation(*m_current); +} + +inline void Game::set_application(const string& name, const string& version) +{ + m_tree.set_application(name, version); +} + +inline void Game::set_bad_move(double value) +{ + m_tree.set_bad_move(*m_current, value); +} + +inline void Game::set_charset(const string& charset) +{ + m_tree.set_charset(charset); +} + +inline void Game::set_comment(const string& s) +{ + m_tree.set_comment(*m_current, s); +} + +inline void Game::set_date(const string& date) +{ + m_tree.set_date(date); +} + +inline void Game::set_event(const string& event) +{ + m_tree.set_event(event); +} + +inline void Game::set_date_today() +{ + m_tree.set_date_today(); +} + +inline void Game::set_doubtful_move() +{ + m_tree.set_doubtful_move(*m_current); +} + +inline void Game::set_good_move(double value) +{ + m_tree.set_good_move(*m_current, value); +} + +inline void Game::set_interesting_move() +{ + m_tree.set_interesting_move(*m_current); +} + +inline void Game::set_modified() +{ + m_tree.set_modified(); +} + +inline void Game::set_player_name(Color c, const string& name) +{ + m_tree.set_player_name(c, name); +} + +inline void Game::set_round(const string& round) +{ + m_tree.set_round(round); +} + +inline void Game::set_time(const string& time) +{ + m_tree.set_time(time); +} + +inline void Game::truncate_children() +{ + m_tree.remove_children(*m_current); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_GAME_H diff --git a/src/libpentobi_base/Geometry.h b/src/libpentobi_base/Geometry.h new file mode 100644 index 0000000..cff9b28 --- /dev/null +++ b/src/libpentobi_base/Geometry.h @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Geometry.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_GEOMETRY_H +#define LIBPENTOBI_BASE_GEOMETRY_H + +#include "Point.h" +#include "libboardgame_base/Geometry.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +typedef libboardgame_base::Geometry Geometry; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_GEOMETRY_H diff --git a/src/libpentobi_base/Grid.h b/src/libpentobi_base/Grid.h new file mode 100644 index 0000000..78d4e72 --- /dev/null +++ b/src/libpentobi_base/Grid.h @@ -0,0 +1,27 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Grid.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_GRID_H +#define LIBPENTOBI_BASE_GRID_H + +#include "Point.h" +#include "libboardgame_base/Grid.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +template +using Grid = libboardgame_base::Grid; + +template +using GridExt = libboardgame_base::GridExt; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_GRID_H diff --git a/src/libpentobi_base/Marker.h b/src/libpentobi_base/Marker.h new file mode 100644 index 0000000..eb867c9 --- /dev/null +++ b/src/libpentobi_base/Marker.h @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Marker.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_MARKER_H +#define LIBPENTOBI_BASE_MARKER_H + +#include "Point.h" +#include "libboardgame_base/Marker.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +typedef libboardgame_base::Marker Marker; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_MARKER_H diff --git a/src/libpentobi_base/Move.h b/src/libpentobi_base/Move.h new file mode 100644 index 0000000..1022ca6 --- /dev/null +++ b/src/libpentobi_base/Move.h @@ -0,0 +1,136 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Move.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_MOVE_H +#define LIBPENTOBI_BASE_MOVE_H + +#include +#include "libboardgame_util/Assert.h" + +namespace libpentobi_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +class Move +{ +public: + /** Integer type used internally in this class to store a move. + This class is optimized for size not for speed because there are + large precomputed data structures that store moves and move lists. + Therefore it uses uint_least16_t, not uint_fast16_t. */ + typedef uint_least16_t IntType; + + static const IntType onboard_moves_classic = 30433; + + static const IntType onboard_moves_trigon = 32131; + + static const IntType onboard_moves_trigon_3 = 24859; + + static const IntType onboard_moves_duo = 13729; + + static const IntType onboard_moves_junior = 7217; + + static const IntType onboard_moves_nexos = 15157; + + static const IntType onboard_moves_callisto = 9433; + + static const IntType onboard_moves_callisto_2 = 4265; + + static const IntType onboard_moves_callisto_3 = 6885; + + /** Integer range of moves. + The maximum is given by the number of on-board moves in game variant + Trigon, plus a null move. */ + static const IntType range = onboard_moves_trigon + 1; + + static Move null(); + + Move(); + + explicit Move(IntType i); + + bool operator==(const Move& mv) const; + + bool operator!=(const Move& mv) const; + + bool operator<(const Move& mv) const; + + bool is_null() const; + + /** Return move as an integer between 0 and Move::range */ + IntType to_int() const; + +private: + static const IntType value_uninitialized = range; + + IntType m_i; + + bool is_initialized() const; +}; + +inline Move::Move() +{ +#if LIBBOARDGAME_DEBUG + m_i = value_uninitialized; +#endif +} + +inline Move::Move(IntType i) +{ + LIBBOARDGAME_ASSERT(i < range); + m_i = i; +} + +inline bool Move::operator==(const Move& mv) const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + LIBBOARDGAME_ASSERT(mv.is_initialized()); + return m_i == mv.m_i; +} + +inline bool Move::operator!=(const Move& mv) const +{ + return ! operator==(mv); +} + +inline bool Move::operator<(const Move& mv) const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + LIBBOARDGAME_ASSERT(mv.is_initialized()); + return m_i < mv.m_i; +} + +inline bool Move::is_initialized() const +{ + return m_i < value_uninitialized; +} + +inline bool Move::is_null() const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + return m_i == 0; +} + +inline Move Move::null() +{ + return Move(0); +} + +inline Move::IntType Move::to_int() const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + return m_i; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_BASE_MOVE_H diff --git a/src/libpentobi_base/MoveInfo.h b/src/libpentobi_base/MoveInfo.h new file mode 100644 index 0000000..3cd8921 --- /dev/null +++ b/src/libpentobi_base/MoveInfo.h @@ -0,0 +1,135 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/MoveInfo.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_MOVE_INFO_H +#define LIBPENTOBI_BASE_MOVE_INFO_H + +#include "Move.h" +#include "MovePoints.h" +#include "Piece.h" +#include "PieceInfo.h" + +namespace libpentobi_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Most frequently accessed move info. + Contains the points and the piece of the move. If the point list is smaller + than MAX_SIZE, values above end() up to MAX_SIZE may be accessed and + contain Point::null() to allow loop unrolling. The points correspond to + PieceInfo::get_points(), which includes certain junction points in Nexos, + see comment there. + Since this is the most performance-critical data structure, it takes + a template argument to make the space for move points not larger than + needed in the current game variant. */ +template +class MoveInfo +{ +public: + MoveInfo() = default; + + MoveInfo(Piece piece, const MovePoints& points) + { + m_piece = static_cast(piece.to_int()); + m_size = static_cast(points.size()); + for (MovePoints::IntType i = 0; i < MAX_SIZE; ++i) + m_points[i] = points.get_unchecked(i); + } + + const Point* begin() const { return m_points; } + + const Point* end() const { return m_points + m_size; } + + Piece get_piece() const { return Piece(m_piece); } + + unsigned get_size() const { return m_size; } + +private: + uint_least8_t m_piece; + + uint_least8_t m_size; + + Point m_points[MAX_SIZE]; +}; + +//----------------------------------------------------------------------------- + +/** Less frequently accessed move info. + Stored separately from move points and move piece to improve CPU cache + performance. + Since this is a performance-critical data structure, it takes + a template argument to make the space for move points not larger than + needed in the current game variant. + @tparam MAX_ADJ_ATTACH Maximum total number of attach points and adjacent + points of a piece in the corresponding game variant. */ +template +struct MoveInfoExt +{ + /** Concatenated list of adjacent and attach points. */ + Point points[MAX_ADJ_ATTACH]; + + uint_least8_t size_attach_points; + + uint_least8_t size_adj_points; + + const Point* begin_adj() const { return points; } + + const Point* end_adj() const { return points + size_adj_points; } + + const Point* begin_attach() const { return end_adj(); } + + const Point* end_attach() const + { + return begin_attach() + size_attach_points; + } +}; + +//----------------------------------------------------------------------------- + +/** Least frequently accessed move info. + Stored separately from move points and move piece to improve CPU cache + performance. */ +struct MoveInfoExt2 +{ + /** Whether the move breaks rotational symmetry of the board. + Currently not initialized for classic and trigon_3 board types because + enforced rotational-symmetric draws are not used in the MCTS search on + these boards (trigon_3 has no 2-player game variant and classic_2 + currently only supports colored starting points, which makes rotational + draws impossible. */ + bool breaks_symmetry; + + uint_least8_t scored_points_size; + + /** The rotational-symmetric counterpart to this move. + Only initalized for game variants that have rotational-symmetric boards + and starting points. */ + Move symmetric_move; + + Point label_pos; + + /** The points of a move that contribute to the score, which excludes + junction points in Nexos. */ + Point scored_points[PieceInfo::max_scored_size]; + + + const Point* begin_scored_points() const { return scored_points; } + + const Point* end_scored_points() const + { + return scored_points + scored_points_size; + } +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_BASE_MOVE_INFO_H diff --git a/src/libpentobi_base/MoveList.h b/src/libpentobi_base/MoveList.h new file mode 100644 index 0000000..9f278e9 --- /dev/null +++ b/src/libpentobi_base/MoveList.h @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/MoveList.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_MOVE_LIST_H +#define LIBPENTOBI_BASE_MOVE_LIST_H + +#include "Move.h" +#include "libboardgame_util/ArrayList.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +/** List that can hold all possible moves, not including Move::null() */ +typedef libboardgame_util::ArrayList MoveList; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_MOVE_LIST_H diff --git a/src/libpentobi_base/MoveMarker.h b/src/libpentobi_base/MoveMarker.h new file mode 100644 index 0000000..5e205af --- /dev/null +++ b/src/libpentobi_base/MoveMarker.h @@ -0,0 +1,72 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/MoveMarker.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_MOVE_MARKER_H +#define LIBPENTOBI_BASE_MOVE_MARKER_H + +#include +#include "Move.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +class MoveMarker +{ +public: + MoveMarker() + { + clear(); + } + + bool operator[](Move mv) const + { + return m_a[mv.to_int()]; + } + + void set(Move mv) + { + m_a[mv.to_int()] = true; + } + + void clear(Move mv) + { + m_a[mv.to_int()] = false; + } + + template + void set(const T& t) + { + for (Move mv : t) + set(mv); + } + + template + void clear(const T& t) + { + for (Move mv : t) + clear(mv); + } + + void set() + { + m_a.fill(true); + } + + void clear() + { + m_a.fill(false); + } + +private: + array m_a; +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_MOVE_MARKER_H diff --git a/src/libpentobi_base/MovePoints.h b/src/libpentobi_base/MovePoints.h new file mode 100644 index 0000000..1383234 --- /dev/null +++ b/src/libpentobi_base/MovePoints.h @@ -0,0 +1,28 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/MovePoints.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_MOVE_POINTS_H +#define LIBPENTOBI_BASE_MOVE_POINTS_H + +#include "PieceInfo.h" +#include "Point.h" +#include "libboardgame_util/ArrayList.h" + +namespace libpentobi_base { + +using libboardgame_util::ArrayList; + +//----------------------------------------------------------------------------- + +typedef ArrayList MovePoints; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_BASE_MOVE_POINTS_H diff --git a/src/libpentobi_base/NexosGeometry.cpp b/src/libpentobi_base/NexosGeometry.cpp new file mode 100644 index 0000000..29df080 --- /dev/null +++ b/src/libpentobi_base/NexosGeometry.cpp @@ -0,0 +1,94 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/NexosGeometry.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "NexosGeometry.h" + +#include "libboardgame_util/Unused.h" + +namespace libpentobi_base { + +using libboardgame_base::CoordPoint; + +//----------------------------------------------------------------------------- + +map> NexosGeometry::s_geometry; + +NexosGeometry::NexosGeometry(unsigned sz) +{ + Geometry::init(sz * 2 - 1, sz * 2 - 1); +} + +const NexosGeometry& NexosGeometry::get(unsigned sz) +{ + auto pos = s_geometry.find(sz); + if (pos != s_geometry.end()) + return *pos->second; + shared_ptr geometry(new NexosGeometry(sz)); + return *s_geometry.insert(make_pair(sz, geometry)).first->second; +} + +auto NexosGeometry::get_adj_coord(int x, int y) const -> AdjCoordList +{ + LIBBOARDGAME_UNUSED(x); + LIBBOARDGAME_UNUSED(y); + return AdjCoordList(); +} + +auto NexosGeometry::get_diag_coord(int x, int y) const -> DiagCoordList +{ + DiagCoordList l; + if (get_point_type(x, y) == 1) + { + l.push_back(CoordPoint(x - 2, y)); + l.push_back(CoordPoint(x + 2, y)); + l.push_back(CoordPoint(x - 1, y - 1)); + l.push_back(CoordPoint(x + 1, y + 1)); + l.push_back(CoordPoint(x - 1, y + 1)); + l.push_back(CoordPoint(x + 1, y - 1)); + } + else if (get_point_type(x, y) == 2) + { + l.push_back(CoordPoint(x, y - 2)); + l.push_back(CoordPoint(x, y + 2)); + l.push_back(CoordPoint(x - 1, y - 1)); + l.push_back(CoordPoint(x + 1, y + 1)); + l.push_back(CoordPoint(x - 1, y + 1)); + l.push_back(CoordPoint(x + 1, y - 1)); + } + return l; +} + +unsigned NexosGeometry::get_period_x() const +{ + return 2; +} + +unsigned NexosGeometry::get_period_y() const +{ + return 2; +} + +unsigned NexosGeometry::get_point_type(int x, int y) const +{ + if (x % 2 == 0) + return y % 2 == 0 ? 0 : 2; + else + return y % 2 == 0 ? 1 : 3; +} + +bool NexosGeometry::init_is_onboard(unsigned x, unsigned y) const +{ + return x < get_width() && y < get_height() && get_point_type(x, y) != 3; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + diff --git a/src/libpentobi_base/NexosGeometry.h b/src/libpentobi_base/NexosGeometry.h new file mode 100644 index 0000000..58c7909 --- /dev/null +++ b/src/libpentobi_base/NexosGeometry.h @@ -0,0 +1,74 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/NexosGeometry.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_NEXOS_GEOMETRY_H +#define LIBPENTOBI_BASE_NEXOS_GEOMETRY_H + +#include +#include +#include "Geometry.h" + +namespace libpentobi_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Geometry as used in the game Nexos. + The points of the board are horizontal or vertical segments and junctions. + Junctions only need to be included in piece definitions if they are + necessary to indicate that the opponent cannot cross the junction + (i.e. if exactly two segments of the piece with the same orientation + connect to the junction). + The coordinates are like: + + 0 1 2 3 4 5 6 ... + 0 + - + - + - + + 1 | | | | + 2 + - + - + - + + 3 | | | | + 4 + - + - + - + + + There are four point types: 0=junction, 1=horizontal segment, 2=vertical + segment, 3=hole surrounded by segments. + To fit with the generalizations used in the Blokus engine, points have no + adjacent points, and points are diagonal to each other if they are segments + that connect to the same junction. */ +class NexosGeometry final + : public Geometry +{ +public: + /** Create or reuse an already created geometry with a given size. + @param sz The number of segments in a row or column. */ + static const NexosGeometry& get(unsigned sz); + + + AdjCoordList get_adj_coord(int x, int y) const override; + + DiagCoordList get_diag_coord(int x, int y) const override; + + unsigned get_point_type(int x, int y) const override; + + unsigned get_period_x() const override; + + unsigned get_period_y() const override; + +protected: + bool init_is_onboard(unsigned x, unsigned y) const override; + +private: + /** Stores already created geometries by size. */ + static map> s_geometry; + + + explicit NexosGeometry(unsigned sz); +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_NEXOS_GEOMETRY_H diff --git a/src/libpentobi_base/NodeUtil.cpp b/src/libpentobi_base/NodeUtil.cpp new file mode 100644 index 0000000..1b7b513 --- /dev/null +++ b/src/libpentobi_base/NodeUtil.cpp @@ -0,0 +1,175 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/NodeUtil.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "NodeUtil.h" + +#include "libboardgame_util/StringUtil.h" + +namespace libpentobi_base { +namespace node_util { + +using libboardgame_sgf::InvalidPropertyValue; +using libboardgame_sgf::InvalidTree; +using libboardgame_util::split; +using libboardgame_util::trim; + +//----------------------------------------------------------------------------- + +bool get_move(const SgfNode& node, Variant variant, Color& c, + MovePoints& points) +{ + string id; + // Pentobi 0.1 used BLUE/YELLOW/RED/GREEN instead of 1/2/3/4 as suggested + // by SGF FF[5]. Pentobi 12.0 erroneosly used 1/2 for two-player Callisto + // instead of B/W. We still want to be able to read files written by older + // versions. They will be converted to the current format by + // PentobiTreeWriter. + if (get_nu_colors(variant) == 2) + { + if (node.has_property("B")) + { + id = "B"; + c = Color(0); + } + else if (node.has_property("W")) + { + id = "W"; + c = Color(1); + } + else if (node.has_property("1")) + { + id = "1"; + c = Color(0); + } + else if (node.has_property("2")) + { + id = "2"; + c = Color(1); + } + else if (node.has_property("BLUE")) + { + id = "BLUE"; + c = Color(0); + } + else if (node.has_property("GREEN")) + { + id = "GREEN"; + c = Color(1); + } + } + else + { + if (node.has_property("1")) + { + id = "1"; + c = Color(0); + } + else if (node.has_property("2")) + { + id = "2"; + c = Color(1); + } + else if (node.has_property("3")) + { + id = "3"; + c = Color(2); + } + else if (node.has_property("4")) + { + id = "4"; + c = Color(3); + } + else if (node.has_property("BLUE")) + { + id = "BLUE"; + c = Color(0); + } + else if (node.has_property("YELLOW")) + { + id = "YELLOW"; + c = Color(1); + } + else if (node.has_property("RED")) + { + id = "RED"; + c = Color(2); + } + else if (node.has_property("GREEN")) + { + id = "GREEN"; + c = Color(3); + } + } + if (id.empty()) + return false; + vector values; + values = node.get_multi_property(id); + // Note: we still support having the points of a move in a list of point + // values instead of a single value as used by Pentobi <= 0.2, but it + // is deprecated + points.clear(); + auto& geo = get_geometry(variant); + bool is_nexos = (get_board_type(variant) == BoardType::nexos); + for (const auto& s : values) + { + if (trim(s).empty()) + continue; + vector v = split(s, ','); + for (const auto& p_str : v) + { + Point p; + if (! geo.from_string(p_str, p)) + throw InvalidPropertyValue(id, p_str); + if (is_nexos) + { + auto point_type = geo.get_point_type(p); + if (point_type != 1 && point_type != 2) + // Silently discard points that are not line segments, such + // files were written by some (unreleased) versions of + // Pentobi. + continue; + } + points.push_back(p); + } + } + return true; +} + +bool get_player(const SgfNode& node, Color& c) +{ + if (! node.has_property("PL")) + return false; + string value = node.get_property("PL"); + if (value == "B" || value == "1") + c = Color(0); + else if (value == "W" || value == "2") + c = Color(1); + else if (value == "3") + c = Color(2); + else if (value == "4") + c = Color(3); + else + throw InvalidTree("invalid value for PL property"); + return true; +} + +bool has_setup(const SgfNode& node) +{ + for (auto& i : node.get_properties()) + if (i.id == "AB" || i.id == "AW" || i.id == "A1" || i.id == "A2" + || i.id == "A3" || i.id == "A4" || i.id == "AE") + return true; + return false; +} + +//----------------------------------------------------------------------------- + +} // namespace node_util +} // namespace libpentobi_base diff --git a/src/libpentobi_base/NodeUtil.h b/src/libpentobi_base/NodeUtil.h new file mode 100644 index 0000000..dab324d --- /dev/null +++ b/src/libpentobi_base/NodeUtil.h @@ -0,0 +1,43 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/NodeUtil.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_NODE_UTIL_H +#define LIBPENTOBI_BASE_NODE_UTIL_H + +#include "Color.h" +#include "MovePoints.h" +#include "Variant.h" +#include "libboardgame_sgf/SgfNode.h" + +namespace libpentobi_base { +namespace node_util { + +using libboardgame_sgf::SgfNode; + +//----------------------------------------------------------------------------- + +/** Get move points. + @param node + @param variant + @param[out] c The move color (only defined if return value is true) + @param[out] points The move points (only defined if return value is + true) + @return true if the node has a move property. */ +bool get_move(const SgfNode& node, Variant variant, Color& c, + MovePoints& points); + +/** Check if a node has setup properties (not including the PL property). */ +bool has_setup(const SgfNode& node); + +/** Get the color to play in a setup position (PL property). */ +bool get_player(const SgfNode& node, Color& c); + +//----------------------------------------------------------------------------- + +} // namespace node_util +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_NODE_UTIL_H diff --git a/src/libpentobi_base/PentobiSgfUtil.cpp b/src/libpentobi_base/PentobiSgfUtil.cpp new file mode 100644 index 0000000..6a09123 --- /dev/null +++ b/src/libpentobi_base/PentobiSgfUtil.cpp @@ -0,0 +1,53 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PentobiSgfUtil.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "PentobiSgfUtil.h" + +#include "libboardgame_util/Assert.h" + +namespace libpentobi_base { +namespace sgf_util { + +//----------------------------------------------------------------------------- + +const char* get_color_id(Variant variant, Color c) +{ + static_assert(Color::range == 4, ""); + if (get_nu_colors(variant) == 2) + return c == Color(0) ? "B" : "W"; + if (c == Color(0)) + return "1"; + if (c == Color(1)) + return "2"; + if (c == Color(2)) + return "3"; + LIBBOARDGAME_ASSERT(c == Color(3)); + return "4"; +} + +const char* get_setup_id(Variant variant, Color c) +{ + static_assert(Color::range == 4, ""); + if (get_nu_colors(variant) == 2) + return c == Color(0) ? "AB" : "AW"; + if (c == Color(0)) + return "A1"; + if (c == Color(1)) + return "A2"; + if (c == Color(2)) + return "A3"; + LIBBOARDGAME_ASSERT(c == Color(3)); + return "A4"; +} + +//----------------------------------------------------------------------------- + +} // namespace sgf_util +} // namespace libpentobi_base diff --git a/src/libpentobi_base/PentobiSgfUtil.h b/src/libpentobi_base/PentobiSgfUtil.h new file mode 100644 index 0000000..dc6111e --- /dev/null +++ b/src/libpentobi_base/PentobiSgfUtil.h @@ -0,0 +1,29 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PentobiSgfUtil.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_PENTOBI_SGF_UTIL_H +#define LIBPENTOBI_BASE_PENTOBI_SGF_UTIL_H + +#include "Color.h" +#include "Variant.h" + +namespace libpentobi_base { +namespace sgf_util { + +//----------------------------------------------------------------------------- + +/** Get SGF move property ID for a color in a game variant. */ +const char* get_color_id(Variant variant, Color c); + +/** Get SGF setup property ID for a color in a game variant. */ +const char* get_setup_id(Variant variant, Color c); + +//----------------------------------------------------------------------------- + +} // namespace sgf_util +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_PENTOBI_SGF_UTIL_H diff --git a/src/libpentobi_base/PentobiTree.cpp b/src/libpentobi_base/PentobiTree.cpp new file mode 100644 index 0000000..b684b5e --- /dev/null +++ b/src/libpentobi_base/PentobiTree.cpp @@ -0,0 +1,365 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PentobiTree.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "PentobiTree.h" + +#include "BoardUpdater.h" +#include "BoardUtil.h" +#include "NodeUtil.h" +#include "libboardgame_util/StringUtil.h" + +namespace libpentobi_base { + +using libboardgame_sgf::InvalidPropertyValue; +using libboardgame_sgf::InvalidTree; +using libboardgame_util::to_string; +using libpentobi_base::boardutil::get_current_position_as_setup; + +//----------------------------------------------------------------------------- + +PentobiTree::PentobiTree(Variant variant) +{ + init_variant(variant); +} + +PentobiTree::PentobiTree(unique_ptr& root) +{ + init(root); +} + +const SgfNode& PentobiTree::add_setup(const SgfNode& node, Color c, Move mv) +{ + const SgfNode* result; + if (has_move(node)) + result = &create_new_child(node); + else + result = &node; + Setup::PlacementList add_empty = get_setup_property(*result, "AE"); + if (add_empty.remove(mv)) + set_setup_property(*result, "AE", add_empty); + auto id = get_setup_prop_id(c); + Setup::PlacementList add_color = get_setup_property(*result, id); + if (add_color.include(mv)) + set_setup_property(*result, id, add_color); + return *result; +} + +const SgfNode* PentobiTree::find_child_with_move(const SgfNode& node, + ColorMove mv) const +{ + for (auto& i : node.get_children()) + if (get_move(i) == mv) + return &i; + return nullptr; +} + +ColorMove PentobiTree::get_move(const SgfNode& node) const +{ + Color c; + MovePoints points; + if (! libpentobi_base::node_util::get_move(node, m_variant, c, points)) + return ColorMove::null(); + if (points.size() == 0) + // Older (unreleased?) versions of Pentobi used empty move values + // to encode pass moves in search tree dumps but we don't support + // pass moves Board anymore. + return ColorMove::null(); + Move mv; + if (! m_bc->find_move(points, mv)) + throw InvalidTree("Tree contains illegal move"); + return ColorMove(c, mv); +} + +ColorMove PentobiTree::get_move_ignore_invalid(const SgfNode& node) const +{ + try + { + return get_move(node); + } + catch (const InvalidTree&) + { + return ColorMove::null(); + } +} + +const SgfNode* PentobiTree::get_node_before_move_number( + unsigned move_number) const +{ + auto node = &get_root(); + unsigned n = 0; + while (node->has_children()) + { + auto& child = node->get_first_child(); + if (! get_move(child).is_null() && n++ == move_number) + return node; + node = &child; + } + return nullptr; +} + +string PentobiTree::get_player_name(Color c) const +{ + string name; + auto& root = get_root(); + if (get_nu_players(m_variant) == 2) + { + if (c == Color(0) || c == Color(2)) + name = root.get_property("PB", ""); + else if (c == Color(1) || c == Color(2)) + name = root.get_property("PW", ""); + } + else + { + if (c == Color(0)) + name = root.get_property("P1", ""); + else if (c == Color(1)) + name = root.get_property("P2", ""); + else if (c == Color(2)) + name = root.get_property("P3", ""); + else if (c == Color(3)) + name = root.get_property("P4", ""); + } + return name; +} + +Setup::PlacementList PentobiTree::get_setup_property(const SgfNode& node, + const char* id) const +{ + vector values = node.get_multi_property(id); + Setup::PlacementList result; + for (const string& s : values) + result.push_back(m_bc->from_string(s)); + return result; +} + +Variant PentobiTree::get_variant(const SgfNode& root) +{ + string game = root.get_property("GM"); + Variant variant; + if (! parse_variant(game, variant)) + throw InvalidPropertyValue("GM", game); + return variant; +} + +bool PentobiTree::has_main_variation_moves() const +{ + auto node = &get_root(); + while (node) + { + if (has_move_ignore_invalid(*node)) + return true; + node = node->get_first_child_or_null(); + } + return false; +} + +void PentobiTree::init(unique_ptr& root) +{ + Variant variant = get_variant(*root); + SgfTree::init(root); + m_variant = variant; + init_board_const(variant); +} + +void PentobiTree::init_board_const(Variant variant) +{ + m_bc = &BoardConst::get(variant); +} + +void PentobiTree::init_variant(Variant variant) +{ + SgfTree::init(); + m_variant = variant; + set_game_property(); + init_board_const(variant); + clear_modified(); +} + +void PentobiTree::keep_only_subtree(const SgfNode& node) +{ + LIBBOARDGAME_ASSERT(contains(node)); + if (&node == &get_root()) + return; + string charset = get_root().get_property("CA", ""); + string application = get_root().get_property("AP", ""); + bool create_new_setup = has_move(node); + if (! create_new_setup) + { + auto current = node.get_parent_or_null(); + while (current) + { + if (has_move(*current) || node_util::has_setup(*current)) + { + create_new_setup = true; + break; + } + current = current->get_parent_or_null(); + } + } + if (create_new_setup) + { + unique_ptr bd(new Board(m_variant)); + BoardUpdater updater; + updater.update(*bd, *this, node); + Setup setup; + get_current_position_as_setup(*bd, setup); + LIBBOARDGAME_ASSERT(! node_util::has_setup(node)); + set_setup(node, setup); + } + make_root(node); + if (! application.empty()) + { + set_property(node, "AP", application); + move_property_to_front(node, "AP"); + } + if (! charset.empty()) + { + set_property(node, "CA", charset); + move_property_to_front(node, "CA"); + } + set_game_property(); +} + +void PentobiTree::remove_player(const SgfNode& node) +{ + remove_property(node, "PL"); +} + +const SgfNode& PentobiTree::remove_setup(const SgfNode& node, Color c, + Move mv) +{ + const SgfNode* result; + if (has_move(node)) + result = &create_new_child(node); + else + result = &node; + auto id = get_setup_prop_id(c); + auto add_color = get_setup_property(*result, id); + if (add_color.remove(mv)) + set_setup_property(*result, id, add_color); + else + { + Setup::PlacementList add_empty = get_setup_property(*result, "AE"); + if (add_empty.include(mv)) + set_setup_property(*result, "AE", add_empty); + } + return *result; +} + +void PentobiTree::set_game_property() +{ + auto& root = get_root(); + set_property(root, "GM", to_string(m_variant)); + move_property_to_front(root, "GM"); +} + +void PentobiTree::set_move(const SgfNode& node, Color c, Move mv) +{ + LIBBOARDGAME_ASSERT(! mv.is_null()); + auto id = get_color(c); + set_property(node, id, m_bc->to_string(mv, false)); +} + +void PentobiTree::set_player(const SgfNode& node, Color c) +{ + set_property(node, "PL", get_color(c)); +} + +void PentobiTree::set_player_name(Color c, const string& name) +{ + auto& root = get_root(); + if (get_nu_players(m_variant) == 2) + { + if (c == Color(0) || c == Color(2)) + set_property_remove_empty(root, "PB", name); + else if (c == Color(1) || c == Color(3)) + set_property_remove_empty(root, "PW", name); + } + else + { + if (c == Color(0)) + set_property_remove_empty(root, "P1", name); + else if (c == Color(1)) + set_property_remove_empty(root, "P2", name); + else if (c == Color(2)) + set_property_remove_empty(root, "P3", name); + else if (c == Color(3)) + set_property_remove_empty(root, "P4", name); + } +} + +void PentobiTree::set_result(const SgfNode& node, int score) +{ + if (score > 0) + { + ostringstream s; + s << "B+" << score; + set_property(node, "RE", s.str()); + } + else if (score < 0) + { + ostringstream s; + s << "W+" << (-score); + set_property(node, "RE", s.str()); + } + else + set_property(node, "RE", "0"); +} + +void PentobiTree::set_setup(const SgfNode& node, const Setup& setup) +{ + auto nu_colors = get_nu_colors(m_variant); + LIBBOARDGAME_ASSERT(nu_colors >= 2 && nu_colors <= 4); + remove_property(node, "B"); + remove_property(node, "W"); + remove_property(node, "1"); + remove_property(node, "2"); + remove_property(node, "3"); + remove_property(node, "4"); + remove_property(node, "AB"); + remove_property(node, "AW"); + remove_property(node, "A1"); + remove_property(node, "A2"); + remove_property(node, "A3"); + remove_property(node, "A4"); + remove_property(node, "AE"); + if (nu_colors == 2) + { + set_setup_property(node, "AB", setup.placements[Color(0)]); + set_setup_property(node, "AW", setup.placements[Color(1)]); + } + else + { + set_setup_property(node, "A1", setup.placements[Color(0)]); + set_setup_property(node, "A2", setup.placements[Color(1)]); + set_setup_property(node, "A3", setup.placements[Color(2)]); + if (nu_colors > 3) + set_setup_property(node, "A4", setup.placements[Color(3)]); + } + set_player(node, setup.to_play); +} + +void PentobiTree::set_setup_property(const SgfNode& node, const char* id, + const Setup::PlacementList& placements) +{ + if (placements.empty()) + { + remove_property(node, id); + return; + } + vector values; + for (Move mv : placements) + values.push_back(m_bc->to_string(mv, false)); + set_property(node, id, values); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/PentobiTree.h b/src/libpentobi_base/PentobiTree.h new file mode 100644 index 0000000..213280a --- /dev/null +++ b/src/libpentobi_base/PentobiTree.h @@ -0,0 +1,168 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PentobiTree.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_PENTOBI_TREE_H +#define LIBPENTOBI_BASE_PENTOBI_TREE_H + +#include "ColorMove.h" +#include "BoardConst.h" +#include "Variant.h" +#include "Setup.h" +#include "PentobiSgfUtil.h" +#include "libboardgame_sgf/SgfTree.h" + +namespace libpentobi_base { + +using namespace std; +using libboardgame_sgf::SgfNode; +using libboardgame_sgf::SgfTree; + +//----------------------------------------------------------------------------- + +/** Blokus SGF tree. + See also doc/blksgf/Pentobi-SGF.html in the Pentobi distribution for + a description of the properties used. */ +class PentobiTree + : public SgfTree +{ +public: + /** Parse the GM property of a root node. + @throws MissingProperty + @throws InvalidPropertyValue */ + static Variant get_variant(const SgfNode& root); + + + explicit PentobiTree(Variant variant); + + explicit PentobiTree(unique_ptr& root); + + void init(unique_ptr& root) override; + + void init_variant(Variant variant); + + void set_move(const SgfNode& node, ColorMove mv); + + void set_move(const SgfNode& node, Color c, Move mv); + + /** Return move or ColorMove::null() if node has no move property. + @throws InvalidTree if the node has a move property with an invalid + value. */ + ColorMove get_move(const SgfNode& node) const; + + /** Like get_move() but returns ColorMove::null() on invalid property + value. */ + ColorMove get_move_ignore_invalid(const SgfNode& node) const; + + /** Same as ! get_move.is_null() */ + bool has_move(const SgfNode& node) const; + + /** Same as ! get_move_ignore_invalid.is_null() */ + bool has_move_ignore_invalid(const SgfNode& node) const; + + const SgfNode* find_child_with_move(const SgfNode& node, + ColorMove mv) const; + + void set_result(const SgfNode& node, int score); + + const SgfNode* get_node_before_move_number(unsigned move_number) const; + + Variant get_variant() const; + + string get_player_name(Color c) const; + + void set_player_name(Color c, const string& name); + + const BoardConst& get_board_const() const; + + /** Check if any node in the main variation has a move. + Invalid move properties are ignored. */ + bool has_main_variation_moves() const; + + void keep_only_subtree(const SgfNode& node); + + /** Add a piece as setup. + @pre ! mv.is_null() + If the node already contains a move, a new child will be created. + @pre The piece points must be empty on the board + @return The node or the new child if one was created. */ + const SgfNode& add_setup(const SgfNode& node, Color c, Move mv); + + /** Remove a piece using setup properties. + @pre ! mv.is_null() + If the node already contains a move, a new child will be created. + @pre The move must exist on the board + @return The node or the new child if one was created. */ + const SgfNode& remove_setup(const SgfNode& node, Color c, Move mv); + + /** Set the color to play in a setup position (PL property). */ + void set_player(const SgfNode& node, Color c); + + /** Remove the PL property. + @see set_player() */ + void remove_player(const SgfNode& node); + +private: + Variant m_variant; + + const BoardConst* m_bc; + + const char* get_color(Color c) const; + + Setup::PlacementList get_setup_property(const SgfNode& node, + const char* id) const; + + const char* get_setup_prop_id(Color c) const; + + void set_setup(const SgfNode& node, const Setup& setup); + + void init_board_const(Variant variant); + + void set_game_property(); + + void set_setup_property(const SgfNode& node, const char* id, + const Setup::PlacementList& placements); +}; + +inline const BoardConst& PentobiTree::get_board_const() const +{ + return *m_bc; +} + +inline const char* PentobiTree::get_color(Color c) const +{ + return sgf_util::get_color_id(m_variant, c); +} + +inline const char* PentobiTree::get_setup_prop_id(Color c) const +{ + return sgf_util::get_setup_id(m_variant, c); +} + +inline Variant PentobiTree::get_variant() const +{ + return m_variant; +} + +inline bool PentobiTree::has_move(const SgfNode& node) const +{ + return ! get_move(node).is_null(); +} + +inline bool PentobiTree::has_move_ignore_invalid(const SgfNode& node) const +{ + return ! get_move_ignore_invalid(node).is_null(); +} + +inline void PentobiTree::set_move(const SgfNode& node, ColorMove mv) +{ + set_move(node, mv.color, mv.move); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_PENTOBI_SGF_TREE_H diff --git a/src/libpentobi_base/PentobiTreeWriter.cpp b/src/libpentobi_base/PentobiTreeWriter.cpp new file mode 100644 index 0000000..660b4b3 --- /dev/null +++ b/src/libpentobi_base/PentobiTreeWriter.cpp @@ -0,0 +1,82 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PentobiTreeWriter.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "PentobiTreeWriter.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +PentobiTreeWriter::PentobiTreeWriter(ostream& out, const PentobiTree& tree) + : libboardgame_sgf::TreeWriter(out, tree.get_root()), + m_variant(tree.get_variant()) +{ +} + +PentobiTreeWriter::~PentobiTreeWriter() +{ +} + +void PentobiTreeWriter::write_property(const string& id, + const vector& values) +{ + auto nu_colors = get_nu_colors(m_variant); + // Replace obsolete move property IDs or multi-valued move properties + // as used by early versions of Pentobi + if (id == "BLUE" || id == "YELLOW" || id == "GREEN" || id == "RED" + || ((id == "1" || id == "2" || id == "3" || id == "4" || id == "B" + || id == "W") + && values.size() > 1)) + { + string new_id; + if (id == "BLUE") + new_id = (nu_colors == 2 ? "B" : "1"); + else if (id == "YELLOW") + new_id = "2"; + else if (id == "GREEN") + new_id = (nu_colors == 2 ? "W" : "4"); + else if (id == "RED") + new_id = "3"; + else + new_id = id; + if (values.size() < 2) + libboardgame_sgf::TreeWriter::write_property(new_id, values); + else + { + string val = values[0]; + for (size_t i = 1; i < values.size(); ++i) + val += "," + values[i]; + vector new_values; + new_values.push_back(val); + libboardgame_sgf::TreeWriter::write_property(new_id, new_values); + } + return; + } + // Pentobi 12.0 versions erroneously used multi-player properties for + // two-player Callisto. + if (nu_colors == 2) + { + if (id == "1") + { + libboardgame_sgf::TreeWriter::write_property("B", values); + return; + } + if (id == "2") + { + libboardgame_sgf::TreeWriter::write_property("W", values); + return; + } + } + libboardgame_sgf::TreeWriter::write_property(id, values); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/PentobiTreeWriter.h b/src/libpentobi_base/PentobiTreeWriter.h new file mode 100644 index 0000000..bd463a1 --- /dev/null +++ b/src/libpentobi_base/PentobiTreeWriter.h @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PentobiTreeWriter.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_PENTOBI_TREE_WRITER_H +#define LIBPENTOBI_BASE_PENTOBI_TREE_WRITER_H + +#include "PentobiTree.h" +#include "libboardgame_sgf/TreeWriter.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +/** Blokus-specific tree writer. + Automatically replaces obsolete move properties as used by early versions + of Pentobi. */ +class PentobiTreeWriter + : public libboardgame_sgf::TreeWriter +{ +public: + PentobiTreeWriter(ostream& out, const PentobiTree& tree); + + virtual ~PentobiTreeWriter(); + + void write_property(const string& id, + const vector& values) override; + +private: + Variant m_variant; +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_PENTOBI_TREE_WRITER_H diff --git a/src/libpentobi_base/Piece.h b/src/libpentobi_base/Piece.h new file mode 100644 index 0000000..1aa3a29 --- /dev/null +++ b/src/libpentobi_base/Piece.h @@ -0,0 +1,110 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Piece.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_PIECE_H +#define LIBPENTOBI_BASE_PIECE_H + +#include "libboardgame_util/Assert.h" + +namespace libpentobi_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Wrapper around an integer representing a piece type in a certain + game variant. */ +class Piece +{ +public: + typedef uint_fast8_t IntType; + + /** Maximum number of unique pieces per color. */ + static const IntType max_pieces = 24; + + /** Integer range used for unique pieces without the null piece. */ + static const IntType range_not_null = max_pieces; + + /** Integer range used for unique pieces including the null piece */ + static const IntType range = max_pieces + 1; + + static Piece null(); + + Piece(); + + explicit Piece(IntType i); + + bool operator==(const Piece& piece) const; + + bool operator!=(const Piece& piece) const; + + bool is_null() const; + + /** Return move as an integer between 0 and Piece::range */ + IntType to_int() const; + +private: + static const IntType value_null = range - 1; + + static const IntType value_uninitialized = range; + + IntType m_i; + + bool is_initialized() const; +}; + +inline Piece::Piece() +{ +#if LIBBOARDGAME_DEBUG + m_i = value_uninitialized; +#endif +} + +inline Piece::Piece(IntType i) +{ + LIBBOARDGAME_ASSERT(i < range); + m_i = i; +} + +inline bool Piece::operator==(const Piece& piece) const +{ + return m_i == piece.m_i; +} + +inline bool Piece::operator!=(const Piece& piece) const +{ + return ! operator==(piece); +} + +inline bool Piece::is_initialized() const +{ + return m_i < value_uninitialized; +} + +inline bool Piece::is_null() const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + return m_i == value_null; +} + +inline Piece Piece::null() +{ + return Piece(value_null); +} + +inline auto Piece::to_int() const -> IntType +{ + LIBBOARDGAME_ASSERT(is_initialized()); + return m_i; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_BASE_PIECE_H diff --git a/src/libpentobi_base/PieceInfo.cpp b/src/libpentobi_base/PieceInfo.cpp new file mode 100644 index 0000000..663fb35 --- /dev/null +++ b/src/libpentobi_base/PieceInfo.cpp @@ -0,0 +1,231 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PieceInfo.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "PieceInfo.h" + +#include +#include "libboardgame_base/GeometryUtil.h" +#include "libboardgame_util/Assert.h" +#include "libboardgame_util/Log.h" + +namespace libpentobi_base { + +using libboardgame_base::geometry_util::normalize_offset; +using libboardgame_base::geometry_util::type_match_shift; + +//----------------------------------------------------------------------------- + +namespace { + +const bool log_piece_creation = false; + +struct NormalizedPoints +{ + /** The normalized points of the transformed piece. + The points were shifted using GeometryUtil::normalize_offset(). */ + PiecePoints points; + + /** The point type of (0,0) in the normalized points. */ + unsigned point_type; + + bool operator==(const NormalizedPoints& n) const + { + return points == n.points && point_type == n.point_type; + } +}; + +#if LIBBOARDGAME_DEBUG +/** Check consistency of transformations. + Checks that the point list (which must be already sorted) has no + duplicates. */ +bool check_consistency(const PiecePoints& points) +{ + for (unsigned i = 0; i < points.size(); ++i) + if (i > 0 && points[i] == points[i - 1]) + return false; + return true; +} +#endif // LIBBOARDGAME_DEBUG + +/** Bring piece points into a normal form that is constant under translation. */ +NormalizedPoints normalize(const PiecePoints& points, unsigned point_type, + const Geometry& geo) +{ + if (log_piece_creation) + LIBBOARDGAME_LOG("Points ", points); + NormalizedPoints normalized; + normalized.points = points; + type_match_shift(geo, normalized.points.begin(), + normalized.points.end(), point_type); + if (log_piece_creation) + LIBBOARDGAME_LOG("Point type ", point_type, ", type match shift ", + normalized.points); + // Make the coordinates positive and minimal + unsigned width; // unused + unsigned height; // unused + CoordPoint offset; + normalize_offset(normalized.points.begin(), normalized.points.end(), + width, height, offset); + normalized.point_type = geo.get_point_type(offset); + // Sort the coordinates + sort(normalized.points.begin(), normalized.points.end()); + return normalized; +} + +} // namespace + +//----------------------------------------------------------------------------- + +PieceInfo::PieceInfo(const string& name, const PiecePoints& points, + const Geometry& geo, const PieceTransforms& transforms, + PieceSet piece_set, const CoordPoint& label_pos, + unsigned nu_instances) + : m_nu_instances(nu_instances), + m_points(points), + m_label_pos(label_pos), + m_transforms(&transforms), + m_name(name) +{ + LIBBOARDGAME_ASSERT(nu_instances > 0); + LIBBOARDGAME_ASSERT(nu_instances <= PieceInfo::max_instances); + if (log_piece_creation) + LIBBOARDGAME_LOG("Creating transformations for piece ", name, ' ', + points); + vector all_transformed_points; + PiecePoints transformed_points; + for (const Transform* transform : transforms.get_all()) + { + if (log_piece_creation) + LIBBOARDGAME_LOG("Transformation ", typeid(*transform).name()); + transformed_points = points; + transform->transform(transformed_points.begin(), + transformed_points.end()); + NormalizedPoints normalized = normalize(transformed_points, + transform->get_new_point_type(), + geo); + if (log_piece_creation) + LIBBOARDGAME_LOG("Normalized ", normalized.points, " point type ", + normalized.point_type); + LIBBOARDGAME_ASSERT(check_consistency(normalized.points)); + auto begin = all_transformed_points.begin(); + auto end = all_transformed_points.end(); + auto pos = find(begin, end, normalized); + if (pos != end) + { + if (log_piece_creation) + LIBBOARDGAME_LOG("Equivalent to ", pos - begin); + m_equivalent_transform[transform] + = transforms.get_all()[pos - begin]; + } + else + { + if (log_piece_creation) + LIBBOARDGAME_LOG("New (", m_uniq_transforms.size(), ")"); + m_equivalent_transform[transform] = transform; + m_uniq_transforms.push_back(transform); + } + all_transformed_points.push_back(normalized); + }; + if (piece_set == PieceSet::nexos) + { + m_score_points = 0; + for (auto& p : points) + { + auto point_type = geo.get_point_type(p); + LIBBOARDGAME_ASSERT(point_type <= 2); + if (point_type == 1 || point_type == 2) // Line segment + ++m_score_points; + } + } + else if (points.size() == 1 && piece_set == PieceSet::callisto) + m_score_points = 0; + else + m_score_points = static_cast(points.size()); +} + +bool PieceInfo::can_flip_horizontally(const Transform* transform) const +{ + transform = get_equivalent_transform(transform); + auto flip = get_equivalent_transform( + m_transforms->get_mirrored_horizontally(transform)); + return flip != transform; +} + +bool PieceInfo::can_flip_vertically(const Transform* transform) const +{ + transform = get_equivalent_transform(transform); + auto flip = get_equivalent_transform( + m_transforms->get_mirrored_vertically(transform)); + return flip != transform; +} + +bool PieceInfo::can_rotate() const +{ + auto transform = m_uniq_transforms[0]; + auto rotate = get_equivalent_transform( + m_transforms->get_rotated_clockwise(transform)); + return rotate != transform; +} + +const Transform* PieceInfo::find_transform(const Geometry& geo, + const Points& points) const +{ + NormalizedPoints normalized = + normalize(points, geo.get_point_type(0, 0), geo); + for (const Transform* transform : get_transforms()) + { + Points piece_points = get_points(); + transform->transform(piece_points.begin(), piece_points.end()); + NormalizedPoints normalized_piece = + normalize(piece_points, transform->get_new_point_type(), geo); + if (normalized_piece == normalized) + return transform; + } + return nullptr; +} + +const Transform* PieceInfo::get_equivalent_transform( + const Transform* transform) const +{ + auto pos = m_equivalent_transform.find(transform); + LIBBOARDGAME_ASSERT(pos != m_equivalent_transform.end()); + return pos->second; +} + +const Transform* PieceInfo::get_next_transform(const Transform* transform) const +{ + transform = get_equivalent_transform(transform); + auto begin = m_uniq_transforms.begin(); + auto end = m_uniq_transforms.end(); + auto pos = find(begin, end, transform); + LIBBOARDGAME_ASSERT(pos != end); + if (pos + 1 == end) + return *begin; + else + return *(pos + 1); +} + +const Transform* PieceInfo::get_previous_transform( + const Transform* transform) const +{ + transform = get_equivalent_transform(transform); + auto begin = m_uniq_transforms.begin(); + auto end = m_uniq_transforms.end(); + auto pos = find(begin, end, transform); + LIBBOARDGAME_ASSERT(pos != end); + if (pos == begin) + return *(end - 1); + else + return *(pos - 1); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/PieceInfo.h b/src/libpentobi_base/PieceInfo.h new file mode 100644 index 0000000..416fb5f --- /dev/null +++ b/src/libpentobi_base/PieceInfo.h @@ -0,0 +1,137 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PieceInfo.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_PIECE_INFO_H +#define LIBPENTOBI_BASE_PIECE_INFO_H + +#include +#include +#include +#include "Geometry.h" +#include "PieceTransforms.h" +#include "Variant.h" +#include "libboardgame_base/CoordPoint.h" +#include "libboardgame_base/Transform.h" +#include "libboardgame_util/ArrayList.h" + +namespace libpentobi_base { + +using namespace std; +using libboardgame_base::CoordPoint; +using libboardgame_base::Transform; +using libboardgame_util::ArrayList; + +//----------------------------------------------------------------------------- + +typedef float ScoreType; + +//----------------------------------------------------------------------------- + +class PieceInfo +{ +public: + /** Maximum number of points in a piece. + The maximum piece size occurs with the I4 piece in Nexos (4 real points + and 3 junction points, see get_points()). */ + static const unsigned max_size = 7; + + /** Maximum number of scored points in a piece. + This excludes junction points in Nexos. The maximum number of scored + points occurs in Trigon. */ + static const unsigned max_scored_size = 6; + + /** Maximum number of instances of a piece per player. */ + static const unsigned max_instances = 3; + + typedef ArrayList Points; + + + /** Constructor. + @param name A short unique name for the piece. + @param points The coordinates of the piece elements. + @param geo + @param transforms + @param piece_set + @param label_pos The coordinates for drawing a label on the piece. + @param nu_instances The number of instances of the piece per player. */ + PieceInfo(const string& name, const Points& points, + const Geometry& geo, const PieceTransforms& transforms, + PieceSet piece_set, const CoordPoint& label_pos, + unsigned nu_instances = 1); + + const string& get_name() const { return m_name; } + + /** The points of the piece. + In Nexos, the points of a piece contain the coordinates of line + segments and of junctions that are essentially needed to mark the + intersection as non-crossable (i.e. junctions that touch exactly two + line segments of the piece with identical orientation. */ + const Points& get_points() const { return m_points; } + + const CoordPoint& get_label_pos() const { return m_label_pos; } + + /** Return the number of points of the piece that contribute to the score. + This excludes any junction points included in the piece definition in + Nexos.*/ + ScoreType get_score_points() const { return m_score_points; } + + unsigned get_nu_instances() const { return m_nu_instances; } + + /** Get a list with unique transformations. + The list has the same order as PieceTransforms::get_all() but + transformations that are equivalent to a previous transformation + (because of a symmetry of the piece) are omitted. */ + const vector& get_transforms() const + { + return m_uniq_transforms; + } + + /** Get next transform from the list of unique transforms. */ + const Transform* get_next_transform(const Transform* transform) const; + + /** Get previous transform from the list of unique transforms. */ + const Transform* get_previous_transform(const Transform* transform) const; + + /** Get the transform from the list of unique transforms that is equivalent + to a given transform. */ + const Transform* get_equivalent_transform(const Transform* transform) const; + + bool can_rotate() const; + + bool can_flip_horizontally(const Transform* transform) const; + + bool can_flip_vertically(const Transform* transform) const; + + const Transform* find_transform(const Geometry& geo, + const Points& points) const; + +private: + unsigned m_nu_instances; + + Points m_points; + + CoordPoint m_label_pos; + + ScoreType m_score_points; + + const PieceTransforms* m_transforms; + + string m_name; + + vector m_uniq_transforms; + + map m_equivalent_transform; +}; + +//----------------------------------------------------------------------------- + +typedef PieceInfo::Points PiecePoints; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_PIECE_INFO_H diff --git a/src/libpentobi_base/PieceMap.h b/src/libpentobi_base/PieceMap.h new file mode 100644 index 0000000..67930c6 --- /dev/null +++ b/src/libpentobi_base/PieceMap.h @@ -0,0 +1,85 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PieceMap.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_PIECE_MAP_H +#define LIBPENTOBI_BASE_PIECE_MAP_H + +#include +#include +#include "Piece.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +/** Container mapping a unique piece to another element type. + The elements must be default-constructible. */ +template +class PieceMap +{ +public: + PieceMap() = default; + + explicit PieceMap(const T& val); + + PieceMap& operator=(const PieceMap& piece_map); + + bool operator==(const PieceMap& piece_map) const; + + T& operator[](Piece piece); + + const T& operator[](Piece piece) const; + + void fill(const T& val); + +private: + array m_a; +}; + +template +inline PieceMap::PieceMap(const T& val) +{ + fill(val); +} + +template +PieceMap& PieceMap::operator=(const PieceMap& piece_map) +{ + copy(piece_map.m_a.begin(), piece_map.m_a.end(), m_a.begin()); + return *this; +} + +template +bool PieceMap::operator==(const PieceMap& piece_map) const +{ + return equal(m_a.begin(), m_a.end(), piece_map.m_a.begin()); +} + +template +inline T& PieceMap::operator[](Piece piece) +{ + LIBBOARDGAME_ASSERT(! piece.is_null()); + return m_a[piece.to_int()]; +} + +template +inline const T& PieceMap::operator[](Piece piece) const +{ + LIBBOARDGAME_ASSERT(! piece.is_null()); + return m_a[piece.to_int()]; +} + +template +void PieceMap::fill(const T& val) +{ + m_a.fill(val); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_PIECE_MAP_H diff --git a/src/libpentobi_base/PieceTransforms.cpp b/src/libpentobi_base/PieceTransforms.cpp new file mode 100644 index 0000000..c67551e --- /dev/null +++ b/src/libpentobi_base/PieceTransforms.cpp @@ -0,0 +1,21 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PieceTransforms.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "PieceTransforms.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +PieceTransforms::~PieceTransforms() = default; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/PieceTransforms.h b/src/libpentobi_base/PieceTransforms.h new file mode 100644 index 0000000..da6a4cb --- /dev/null +++ b/src/libpentobi_base/PieceTransforms.h @@ -0,0 +1,77 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PieceTransforms.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_PIECE_TRANSFORMS_H +#define LIBPENTOBI_PIECE_TRANSFORMS_H + +#include +#include "libboardgame_base/Transform.h" + +namespace libpentobi_base { + +using namespace std; +using libboardgame_base::Transform; + +//----------------------------------------------------------------------------- + +class PieceTransforms +{ +public: + virtual ~PieceTransforms(); + + virtual const Transform* get_mirrored_horizontally( + const Transform* transf) const = 0; + + virtual const Transform* get_mirrored_vertically( + const Transform* transf) const = 0; + + virtual const Transform* get_rotated_anticlockwise( + const Transform* transf) const = 0; + + virtual const Transform* get_rotated_clockwise( + const Transform* transf) const = 0; + + virtual const Transform* get_default() const; + + const vector& get_all() const; + + /** Find the transform by its class. + @tparam T The class of the transform. + @return The pointer to the transform or null if the transforms do not + contain the instance of the given class. */ + template + const Transform* find() const; + +protected: + /** All piece transformations. + Must be initialized in constructor of subclass. */ + vector m_all; +}; + +template +const Transform* PieceTransforms::find() const +{ + for (auto t : m_all) + if (dynamic_cast(t)) + return t; + return nullptr; +} + +inline const Transform* PieceTransforms::get_default() const +{ + return m_all[0]; +} + +inline const vector& PieceTransforms::get_all() const +{ + return m_all; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_PIECE_TRANSFORMS_H diff --git a/src/libpentobi_base/PieceTransformsClassic.cpp b/src/libpentobi_base/PieceTransformsClassic.cpp new file mode 100644 index 0000000..2186613 --- /dev/null +++ b/src/libpentobi_base/PieceTransformsClassic.cpp @@ -0,0 +1,146 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PieceTransformsClassic.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "PieceTransformsClassic.h" + +#include "libboardgame_util/Assert.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +PieceTransformsClassic::PieceTransformsClassic() +{ + m_all.reserve(8); + m_all.push_back(&m_identity); + m_all.push_back(&m_rot90); + m_all.push_back(&m_rot180); + m_all.push_back(&m_rot270); + m_all.push_back(&m_refl); + m_all.push_back(&m_rot90refl); + m_all.push_back(&m_rot180refl); + m_all.push_back(&m_rot270refl); +} + +const Transform* PieceTransformsClassic::get_mirrored_horizontally( + const Transform* transf) const +{ + const Transform* result; + if (transf == &m_identity) + result = &m_refl; + else if (transf == &m_rot90) + result = &m_rot270refl; + else if (transf == &m_rot180) + result = &m_rot180refl; + else if (transf == &m_rot270) + result = &m_rot90refl; + else if (transf == &m_refl) + result = &m_identity; + else if (transf == &m_rot90refl) + result = &m_rot270; + else if (transf == &m_rot180refl) + result = &m_rot180; + else if (transf == &m_rot270refl) + result = &m_rot90; + else + { + LIBBOARDGAME_ASSERT(false); + result = nullptr; + } + return result; +} + +const Transform* PieceTransformsClassic::get_mirrored_vertically( + const Transform* transf) const +{ + const Transform* result; + if (transf == &m_identity) + result = &m_rot180refl; + else if (transf == &m_rot90) + result = &m_rot90refl; + else if (transf == &m_rot180) + result = &m_refl; + else if (transf == &m_rot270) + result = &m_rot270refl; + else if (transf == &m_refl) + result = &m_rot180; + else if (transf == &m_rot90refl) + result = &m_rot90; + else if (transf == &m_rot180refl) + result = &m_identity; + else if (transf == &m_rot270refl) + result = &m_rot270; + else + { + LIBBOARDGAME_ASSERT(false); + result = nullptr; + } + return result; +} + +const Transform* PieceTransformsClassic::get_rotated_anticlockwise( + const Transform* transf) const +{ + const Transform* result; + if (transf == &m_identity) + result = &m_rot270; + else if (transf == &m_rot90) + result = &m_identity; + else if (transf == &m_rot180) + result = &m_rot90; + else if (transf == &m_rot270) + result = &m_rot180; + else if (transf == &m_refl) + result = &m_rot270refl; + else if (transf == &m_rot90refl) + result = &m_refl; + else if (transf == &m_rot180refl) + result = &m_rot90refl; + else if (transf == &m_rot270refl) + result = &m_rot180refl; + else + { + LIBBOARDGAME_ASSERT(false); + result = nullptr; + } + return result; +} + +const Transform* PieceTransformsClassic::get_rotated_clockwise( + const Transform* transf) const +{ + const Transform* result; + if (transf == &m_identity) + result = &m_rot90; + else if (transf == &m_rot90) + result = &m_rot180; + else if (transf == &m_rot180) + result = &m_rot270; + else if (transf == &m_rot270) + result = &m_identity; + else if (transf == &m_refl) + result = &m_rot90refl; + else if (transf == &m_rot90refl) + result = &m_rot180refl; + else if (transf == &m_rot180refl) + result = &m_rot270refl; + else if (transf == &m_rot270refl) + result = &m_refl; + else + { + LIBBOARDGAME_ASSERT(false); + result = nullptr; + } + return result; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/PieceTransformsClassic.h b/src/libpentobi_base/PieceTransformsClassic.h new file mode 100644 index 0000000..e466845 --- /dev/null +++ b/src/libpentobi_base/PieceTransformsClassic.h @@ -0,0 +1,66 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PieceTransformsClassic.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_PIECE_TRANSFORMS_CLASSIC_H +#define LIBPENTOBI_PIECE_TRANSFORMS_CLASSIC_H + +#include "PieceTransforms.h" +#include "libboardgame_base/RectTransform.h" + +namespace libpentobi_base { + +using libboardgame_base::TransfIdentity; +using libboardgame_base::TransfRectRot90; +using libboardgame_base::TransfRectRot180; +using libboardgame_base::TransfRectRot270; +using libboardgame_base::TransfRectRefl; +using libboardgame_base::TransfRectRot90Refl; +using libboardgame_base::TransfRectRot180Refl; +using libboardgame_base::TransfRectRot270Refl; + +//----------------------------------------------------------------------------- + +class PieceTransformsClassic + : public PieceTransforms +{ +public: + PieceTransformsClassic(); + + const Transform* get_mirrored_horizontally( + const Transform* transf) const override; + + const Transform* get_mirrored_vertically( + const Transform* transf) const override; + + const Transform* get_rotated_anticlockwise( + const Transform* transf) const override; + + const Transform* get_rotated_clockwise( + const Transform* transf) const override; + +private: + TransfIdentity m_identity; + + TransfRectRot90 m_rot90; + + TransfRectRot180 m_rot180; + + TransfRectRot270 m_rot270; + + TransfRectRefl m_refl; + + TransfRectRot90Refl m_rot90refl; + + TransfRectRot180Refl m_rot180refl; + + TransfRectRot270Refl m_rot270refl; +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_PIECE_TRANSFORMS_CLASSIC_H diff --git a/src/libpentobi_base/PieceTransformsTrigon.cpp b/src/libpentobi_base/PieceTransformsTrigon.cpp new file mode 100644 index 0000000..2b0f2d3 --- /dev/null +++ b/src/libpentobi_base/PieceTransformsTrigon.cpp @@ -0,0 +1,187 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PieceTransformsTrigon.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "PieceTransformsTrigon.h" + +#include "libboardgame_util/Assert.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +PieceTransformsTrigon::PieceTransformsTrigon() +{ + m_all.reserve(12); + m_all.push_back(&m_identity); + m_all.push_back(&m_rot60); + m_all.push_back(&m_rot120); + m_all.push_back(&m_rot180); + m_all.push_back(&m_rot240); + m_all.push_back(&m_rot300); + m_all.push_back(&m_refl); + m_all.push_back(&m_refl_rot60); + m_all.push_back(&m_refl_rot120); + m_all.push_back(&m_refl_rot180); + m_all.push_back(&m_refl_rot240); + m_all.push_back(&m_refl_rot300); +} + +const Transform* PieceTransformsTrigon::get_default() const +{ + return &m_identity; +} + +const Transform* PieceTransformsTrigon::get_mirrored_horizontally( + const Transform* transf) const +{ + const Transform* result; + if (transf == &m_identity) + result = &m_refl; + else if (transf == &m_rot60) + result = &m_refl_rot300; + else if (transf == &m_rot120) + result = &m_refl_rot240; + else if (transf == &m_rot180) + result = &m_refl_rot180; + else if (transf == &m_rot240) + result = &m_refl_rot120; + else if (transf == &m_rot300) + result = &m_refl_rot60; + else if (transf == &m_refl) + result = &m_identity; + else if (transf == &m_refl_rot60) + result = &m_rot300; + else if (transf == &m_refl_rot120) + result = &m_rot240; + else if (transf == &m_refl_rot180) + result = &m_rot180; + else if (transf == &m_refl_rot240) + result = &m_rot120; + else if (transf == &m_refl_rot300) + result = &m_rot60; + else + { + LIBBOARDGAME_ASSERT(false); + result = nullptr; + } + return result; +} + +const Transform* PieceTransformsTrigon::get_mirrored_vertically( + const Transform* transf) const +{ + const Transform* result; + if (transf == &m_identity) + result = &m_refl_rot180; + else if (transf == &m_rot60) + result = &m_refl_rot120; + else if (transf == &m_rot120) + result = &m_refl_rot60; + else if (transf == &m_rot180) + result = &m_refl; + else if (transf == &m_rot240) + result = &m_refl_rot300; + else if (transf == &m_rot300) + result = &m_refl_rot240; + else if (transf == &m_refl) + result = &m_rot180; + else if (transf == &m_refl_rot60) + result = &m_rot120; + else if (transf == &m_refl_rot120) + result = &m_rot60; + else if (transf == &m_refl_rot180) + result = &m_identity; + else if (transf == &m_refl_rot240) + result = &m_rot300; + else if (transf == &m_refl_rot300) + result = &m_rot240; + else + { + LIBBOARDGAME_ASSERT(false); + result = nullptr; + } + return result; +} + +const Transform* PieceTransformsTrigon::get_rotated_anticlockwise( + const Transform* transf) const +{ + const Transform* result; + if (transf == &m_identity) + result = &m_rot300; + else if (transf == &m_rot60) + result = &m_identity; + else if (transf == &m_rot120) + result = &m_rot60; + else if (transf == &m_rot180) + result = &m_rot120; + else if (transf == &m_rot240) + result = &m_rot180; + else if (transf == &m_rot300) + result = &m_rot240; + else if (transf == &m_refl) + result = &m_refl_rot300; + else if (transf == &m_refl_rot60) + result = &m_refl; + else if (transf == &m_refl_rot120) + result = &m_refl_rot60; + else if (transf == &m_refl_rot180) + result = &m_refl_rot120; + else if (transf == &m_refl_rot240) + result = &m_refl_rot180; + else if (transf == &m_refl_rot300) + result = &m_refl_rot240; + else + { + LIBBOARDGAME_ASSERT(false); + result = nullptr; + } + return result; +} + +const Transform* PieceTransformsTrigon::get_rotated_clockwise( + const Transform* transf) const +{ + const Transform* result; + if (transf == &m_identity) + result = &m_rot60; + else if (transf == &m_rot60) + result = &m_rot120; + else if (transf == &m_rot120) + result = &m_rot180; + else if (transf == &m_rot180) + result = &m_rot240; + else if (transf == &m_rot240) + result = &m_rot300; + else if (transf == &m_rot300) + result = &m_identity; + else if (transf == &m_refl) + result = &m_refl_rot60; + else if (transf == &m_refl_rot60) + result = &m_refl_rot120; + else if (transf == &m_refl_rot120) + result = &m_refl_rot180; + else if (transf == &m_refl_rot180) + result = &m_refl_rot240; + else if (transf == &m_refl_rot240) + result = &m_refl_rot300; + else if (transf == &m_refl_rot300) + result = &m_refl; + else + { + LIBBOARDGAME_ASSERT(false); + result = nullptr; + } + return result; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/PieceTransformsTrigon.h b/src/libpentobi_base/PieceTransformsTrigon.h new file mode 100644 index 0000000..47d94d0 --- /dev/null +++ b/src/libpentobi_base/PieceTransformsTrigon.h @@ -0,0 +1,67 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PieceTransformsTrigon.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_PIECE_TRANSFORMS_TRIGON_H +#define LIBPENTOBI_PIECE_TRANSFORMS_TRIGON_H + +#include "PieceTransforms.h" +#include "TrigonTransform.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +class PieceTransformsTrigon + : public PieceTransforms +{ +public: + PieceTransformsTrigon(); + + const Transform* get_mirrored_horizontally( + const Transform* transf) const override; + + const Transform* get_mirrored_vertically( + const Transform* transf) const override; + + const Transform* get_rotated_anticlockwise( + const Transform* transf) const override; + + const Transform* get_rotated_clockwise( + const Transform* transf) const override; + + const Transform* get_default() const override; + +private: + TransfTrigonIdentity m_identity; + + TransfTrigonRot60 m_rot60; + + TransfTrigonRot120 m_rot120; + + TransfTrigonRot180 m_rot180; + + TransfTrigonRot240 m_rot240; + + TransfTrigonRot300 m_rot300; + + TransfTrigonRefl m_refl; + + TransfTrigonReflRot60 m_refl_rot60; + + TransfTrigonReflRot120 m_refl_rot120; + + TransfTrigonReflRot180 m_refl_rot180; + + TransfTrigonReflRot240 m_refl_rot240; + + TransfTrigonReflRot300 m_refl_rot300; +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_PIECE_TRANSFORMS_TRIGON_H diff --git a/src/libpentobi_base/PlayerBase.cpp b/src/libpentobi_base/PlayerBase.cpp new file mode 100644 index 0000000..d6be112 --- /dev/null +++ b/src/libpentobi_base/PlayerBase.cpp @@ -0,0 +1,28 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PlayerBase.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "PlayerBase.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +PlayerBase::~PlayerBase() +{ +} + +bool PlayerBase::resign() const +{ + return false; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/PlayerBase.h b/src/libpentobi_base/PlayerBase.h new file mode 100644 index 0000000..df9b92a --- /dev/null +++ b/src/libpentobi_base/PlayerBase.h @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PlayerBase.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_PLAYER_BASE_H +#define LIBPENTOBI_BASE_PLAYER_BASE_H + +#include "Board.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +class PlayerBase +{ +public: + virtual ~PlayerBase(); + + virtual Move genmove(const Board& bd, Color c) = 0; + + /** Check if the player wants to resign. + This may only be called after a genmove() and returns true if the + player wants to resign in the position at the last genmove(). + The default implementation returns false. */ + virtual bool resign() const; +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_PLAYER_BASE_H diff --git a/src/libpentobi_base/Point.h b/src/libpentobi_base/Point.h new file mode 100644 index 0000000..6b3dae9 --- /dev/null +++ b/src/libpentobi_base/Point.h @@ -0,0 +1,27 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Point.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_POINT_H +#define LIBPENTOBI_BASE_POINT_H + +#include "libboardgame_base/Point.h" + +//----------------------------------------------------------------------------- + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +/** Point (coordinate of on-board field) for Blokus game variants. + Supports RectGeometry up to size 20, TrigonGeometry up to edge size 9, + and NexosGeometry up to size 13. */ +typedef libboardgame_base::Point<486, 35, 25, unsigned short> Point; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_POINT_H diff --git a/src/libpentobi_base/PointList.h b/src/libpentobi_base/PointList.h new file mode 100644 index 0000000..4cfab9e --- /dev/null +++ b/src/libpentobi_base/PointList.h @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PointList.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_POINT_LIST_H +#define LIBPENTOBI_BASE_POINT_LIST_H + +#include "Point.h" +#include "libboardgame_util/ArrayList.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +typedef ArrayList PointList; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_POINT_LIST_H diff --git a/src/libpentobi_base/PointState.cpp b/src/libpentobi_base/PointState.cpp new file mode 100644 index 0000000..de1267b --- /dev/null +++ b/src/libpentobi_base/PointState.cpp @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PointState.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "PointState.h" + +#include + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +ostream& operator<<(ostream& out, const PointState& s) +{ + if (s.is_color()) + out << s.to_color(); + else + out << 'E'; + return out; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/PointState.h b/src/libpentobi_base/PointState.h new file mode 100644 index 0000000..a950b51 --- /dev/null +++ b/src/libpentobi_base/PointState.h @@ -0,0 +1,142 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PointState.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_POINTSTATE_H +#define LIBPENTOBI_BASE_POINTSTATE_H + +#include "Color.h" + +namespace libpentobi_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** State of an on-board point, which can be a color or empty */ +class PointState +{ +public: + typedef Color::IntType IntType; + + static const IntType range = Color::range + 1; + + static const IntType value_empty = range - 1; + + + PointState(); + + explicit PointState(Color c); + + explicit PointState(IntType i); + + bool operator==(const PointState& s) const; + + bool operator!=(const PointState& s) const; + + bool operator==(const Color& c) const; + + bool operator!=(const Color& c) const; + + IntType to_int() const; + + static PointState empty(); + + bool is_empty() const; + + bool is_color() const; + + Color to_color() const; + +private: + static const IntType value_uninitialized = range; + + IntType m_i; + + bool is_initialized() const; +}; + + +inline PointState::PointState() +{ +#if LIBBOARDGAME_DEBUG + m_i = value_uninitialized; +#endif +} + +inline PointState::PointState(Color c) +{ + m_i = c.to_int(); +} + +inline PointState::PointState(IntType i) +{ + LIBBOARDGAME_ASSERT(i < range); + m_i = i; +} + +inline bool PointState::operator==(const PointState& p) const +{ + return m_i == p.m_i; +} + +inline bool PointState::operator==(const Color& c) const +{ + return m_i == c.to_int(); +} + +inline bool PointState::operator!=(const PointState& s) const +{ + return ! operator==(s); +} + +inline bool PointState::operator!=(const Color& c) const +{ + return ! operator==(c); +} + +inline PointState PointState::empty() +{ + return PointState(value_empty); +} + +inline bool PointState::is_initialized() const +{ + return m_i < value_uninitialized; +} + +inline bool PointState::is_color() const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + return m_i != value_empty; +} + +inline bool PointState::is_empty() const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + return m_i == value_empty; +} + +inline Color PointState::to_color() const +{ + LIBBOARDGAME_ASSERT(is_color()); + return Color(m_i); +} + +inline PointState::IntType PointState::to_int() const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + return m_i; +} + +//----------------------------------------------------------------------------- + +ostream& operator<<(ostream& out, const PointState& s); + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_POINTSTATE_H diff --git a/src/libpentobi_base/PrecompMoves.h b/src/libpentobi_base/PrecompMoves.h new file mode 100644 index 0000000..60a05a9 --- /dev/null +++ b/src/libpentobi_base/PrecompMoves.h @@ -0,0 +1,136 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PrecompMoves.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_PRECOMP_MOVES_H +#define LIBPENTOBI_BASE_PRECOMP_MOVES_H + +#include "Grid.h" +#include "Move.h" +#include "PieceMap.h" +#include "Point.h" +#include "libboardgame_util/Range.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +/** Precomputed moves for fast move generation. + Compact storage of precomputed lists with local moves. Each list contains + all moves that include a given point constrained by the piece type and the + forbidden status of adjacant points. This drastically reduces the number of + moves that need to be checked for legality during move generation. + @see Board::get_adj_status() */ +class PrecompMoves +{ +public: + /** The number of neighbors used for computing the adjacent status. + The adjacent status is a single number that encodes the forbidden + status of the first adj_status_nu_adj neighbors (from the list + Geometry::get_adj() concatenated with Geometry::get_diag()). It is used + for speeding up the matching of moves at a given point. Increasing this + number will make the precomputed lists shorter but exponentially + increase the number of lists and the total memory used for all lists. + Therefore, the optimal value for speeding up the matching depends on + the CPU cache size. */ +#if PENTOBI_LOW_RESOURCES + static const unsigned adj_status_nu_adj = 5; +#else + static const unsigned adj_status_nu_adj = 6; +#endif + + /** The maximum sum of the sizes of all precomputed move lists in any + game variant. */ + static const unsigned max_move_lists_sum_length = + adj_status_nu_adj == 4 ? + 832444 : adj_status_nu_adj == 5 ? 1425934 : 2769060; + static_assert(adj_status_nu_adj >= 4 && adj_status_nu_adj <= 6, ""); + + /** The range of values for the adjacent status. */ + static const unsigned nu_adj_status = 1 << adj_status_nu_adj; + + /** Begin/end range for lists with moves at a given point. */ + typedef libboardgame_util::Range Range; + + + /** Add a move to list during construction. */ + void set_move(unsigned i, Move mv) + { + LIBBOARDGAME_ASSERT(i < max_move_lists_sum_length); + m_move_lists[i] = mv; + } + + /** Store beginning and end of a local move list duing construction. */ + void set_list_range(Point p, unsigned adj_status, Piece piece, + unsigned begin, unsigned size) + { + m_moves_range[p][adj_status][piece] = CompressedRange(begin, size); + } + + /** Get all moves of a piece at a point constrained by the forbidden + status of adjacent points. */ + Range get_moves(Piece piece, Point p, unsigned adj_status = 0) const + { + auto& range = m_moves_range[p][adj_status][piece]; + auto begin = move_lists_begin() + range.begin(); + auto end = begin + range.size(); + return Range(begin, end); + } + + bool has_moves(Piece piece, Point p, unsigned adj_status) const + { + return ! m_moves_range[p][adj_status][piece].empty(); + } + + /** Begin of storage for move lists. + Only needed for special use cases like during an in-place construction + of PrecompMoves for follow-up positions when we need to compare the + index of old iterators with the current get_size() to ensure that + we don't overwrite any old content that we still need to read + during the construction. */ + const Move* move_lists_begin() const { return &(*m_move_lists.begin()); } + +private: + class CompressedRange + { + public: + CompressedRange() = default; + + CompressedRange(unsigned begin, unsigned size) + { + LIBBOARDGAME_ASSERT(begin < max_move_lists_sum_length); + LIBBOARDGAME_ASSERT(begin + size <= max_move_lists_sum_length); + static_assert(max_move_lists_sum_length < (1 << 24), ""); + LIBBOARDGAME_ASSERT(size < (1 << 8)); + m_val = size; + if (size != 0) + m_val |= (begin << 8); + } + + bool empty() const { return m_val == 0; } + + unsigned begin() const { return m_val >> 8; } + + unsigned size() const { return m_val & 0xff; } + + private: + uint_least32_t m_val; + }; + + /** See m_move_lists. */ + Grid, nu_adj_status>> m_moves_range; + + /** Compact representation of lists of moves of a piece at a point + constrained by the forbidden status of adjacent points. + All lists are stored in a single array; m_moves_range contains + information about the actual begin/end indices. */ + array m_move_lists; +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_PRECOMP_MOVES_H diff --git a/src/libpentobi_base/ScoreUtil.h b/src/libpentobi_base/ScoreUtil.h new file mode 100644 index 0000000..a2a661e --- /dev/null +++ b/src/libpentobi_base/ScoreUtil.h @@ -0,0 +1,66 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/ScoreUtil.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_SCORE_UTIL_H +#define LIBPENTOBI_BASE_SCORE_UTIL_H + +#include +#include +#include "Color.h" +#include "PieceInfo.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +/** Convert the result of a multi-player game into a comparable number. + This generalizes the game result of a two-player game (0,0.5,1 for + loss/tie/win) for a game with n \> 2 players. The points are sorted in + ascending order. Each rank r_i (i in 0..n-1) is assigned a value of + r_i/(n-1). If multiple players have the same points, the result value is + the average of all ranks with these points. So being the single winner + still gives the result 1 and being the single loser the result 0. Being the + single winner is better than sharing the best rank, which is better than + getting the second rank, etc. + @return The game result for each player. */ +template +void get_multiplayer_result(unsigned nu_players, + const array& points, + array& result, + bool break_ties) +{ + array adjusted, sorted; + for (Color::IntType i = 0; i < nu_players; ++i) + { + adjusted[i] = points[i]; + if (break_ties) + // Favor later player. The adjustment must be smaller than the + // smallest difference in points (0.5 for GembloQ). + adjusted[i] += 0.001f * i; + sorted[i] = adjusted[i]; + } + sort(sorted.begin(), sorted.begin() + nu_players); + for (Color::IntType i = 0; i < nu_players; ++i) + { + FLOAT sum = 0; + FLOAT n = 0; + FLOAT float_j = 0; + FLOAT factor = 1 / FLOAT(nu_players - 1); + for (unsigned j = 0; j < nu_players; ++j, ++float_j) + if (sorted[j] == adjusted[i]) + { + sum += factor * float_j; + ++n; + } + result[i] = sum / n; + } +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_SCORE_UTIL_H diff --git a/src/libpentobi_base/Setup.h b/src/libpentobi_base/Setup.h new file mode 100644 index 0000000..2a74c64 --- /dev/null +++ b/src/libpentobi_base/Setup.h @@ -0,0 +1,44 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Setup.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_SETUP_H +#define LIBPENTOBI_BASE_SETUP_H + +#include "ColorMap.h" +#include "Move.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +/** Definition of a setup position. + A setup position consists of a number of pieces that are placed at once + (in no particular order) on the board and a color to play next. */ +struct Setup +{ + /** Maximum number of pieces on board per color. */ + static const unsigned max_pieces = 24; + + typedef ArrayList PlacementList; + + Color to_play = Color(0); + + ColorMap placements; + + void clear(); +}; + +inline void Setup::clear() +{ + to_play = Color(0); + for_each_color([&](Color c) { placements[c].clear(); }); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_SETUP_H diff --git a/src/libpentobi_base/StartingPoints.cpp b/src/libpentobi_base/StartingPoints.cpp new file mode 100644 index 0000000..6c61f01 --- /dev/null +++ b/src/libpentobi_base/StartingPoints.cpp @@ -0,0 +1,99 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/StartingPoints.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "StartingPoints.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +void StartingPoints::add_colored_starting_point(const Geometry& geo, + unsigned x, unsigned y, + Color c) +{ + Point p = geo.get_point(x, y); + m_is_colored_starting_point[p] = true; + m_starting_point_color[p] = c; + m_starting_points[c].push_back(p); +} + +void StartingPoints::add_colorless_starting_point(const Geometry& geo, + unsigned x, unsigned y) +{ + Point p = geo.get_point(x, y); + m_is_colorless_starting_point[p] = true; + for_each_color([&](Color c) { + m_starting_points[c].push_back(p); + }); +} + +void StartingPoints::init(Variant variant, const Geometry& geo) +{ + m_is_colored_starting_point.fill(false, geo); + m_is_colorless_starting_point.fill(false, geo); + for_each_color([&](Color c) { + m_starting_points[c].clear(); + }); + switch (get_board_type(variant)) + { + case BoardType::classic: + add_colored_starting_point(geo, 0, 0, Color(0)); + add_colored_starting_point(geo, 19, 0, Color(1)); + add_colored_starting_point(geo, 19, 19, Color(2)); + add_colored_starting_point(geo, 0, 19, Color(3)); + break; + case BoardType::duo: + add_colored_starting_point(geo, 4, 4, Color(0)); + add_colored_starting_point(geo, 9, 9, Color(1)); + break; + case BoardType::trigon: + add_colorless_starting_point(geo, 17, 3); + add_colorless_starting_point(geo, 17, 14); + add_colorless_starting_point(geo, 9, 6); + add_colorless_starting_point(geo, 9, 11); + add_colorless_starting_point(geo, 25, 6); + add_colorless_starting_point(geo, 25, 11); + break; + case BoardType::trigon_3: + add_colorless_starting_point(geo, 15, 2); + add_colorless_starting_point(geo, 15, 13); + add_colorless_starting_point(geo, 7, 5); + add_colorless_starting_point(geo, 7, 10); + add_colorless_starting_point(geo, 23, 5); + add_colorless_starting_point(geo, 23, 10); + break; + case BoardType::nexos: + add_colored_starting_point(geo, 4, 3, Color(0)); + add_colored_starting_point(geo, 3, 4, Color(0)); + add_colored_starting_point(geo, 5, 4, Color(0)); + add_colored_starting_point(geo, 4, 5, Color(0)); + add_colored_starting_point(geo, 20, 3, Color(1)); + add_colored_starting_point(geo, 19, 4, Color(1)); + add_colored_starting_point(geo, 21, 4, Color(1)); + add_colored_starting_point(geo, 20, 5, Color(1)); + add_colored_starting_point(geo, 20, 19, Color(2)); + add_colored_starting_point(geo, 19, 20, Color(2)); + add_colored_starting_point(geo, 21, 20, Color(2)); + add_colored_starting_point(geo, 20, 21, Color(2)); + add_colored_starting_point(geo, 4, 19, Color(3)); + add_colored_starting_point(geo, 3, 20, Color(3)); + add_colored_starting_point(geo, 5, 20, Color(3)); + add_colored_starting_point(geo, 4, 21, Color(3)); + break; + case BoardType::callisto: + case BoardType::callisto_2: + case BoardType::callisto_3: + break; + } +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/StartingPoints.h b/src/libpentobi_base/StartingPoints.h new file mode 100644 index 0000000..acd748f --- /dev/null +++ b/src/libpentobi_base/StartingPoints.h @@ -0,0 +1,81 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/StartingPoints.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_STARTING_POINTS_H +#define LIBPENTOBI_BASE_STARTING_POINTS_H + +#include "Color.h" +#include "ColorMap.h" +#include "Geometry.h" +#include "Grid.h" +#include "Variant.h" +#include "libboardgame_util/ArrayList.h" + +namespace libpentobi_base { + +using libboardgame_util::ArrayList; + +//----------------------------------------------------------------------------- + +class StartingPoints +{ +public: + static const unsigned max_starting_points = 16; + + void init(Variant variant, const Geometry& geo); + + bool is_colored_starting_point(Point p) const; + + bool is_colorless_starting_point(Point p) const; + + Color get_starting_point_color(Point p) const; + + const ArrayList& + get_starting_points(Color c) const; + +private: + Grid m_is_colored_starting_point; + + Grid m_is_colorless_starting_point; + + Grid m_starting_point_color; + + ColorMap> m_starting_points; + + void add_colored_starting_point(const Geometry& geo, unsigned x, + unsigned y, Color c); + + void add_colorless_starting_point(const Geometry& geo, unsigned x, + unsigned y); +}; + +inline Color StartingPoints::get_starting_point_color(Point p) const +{ + LIBBOARDGAME_ASSERT(m_is_colored_starting_point[p]); + return m_starting_point_color[p]; +} + +inline const ArrayList& + StartingPoints::get_starting_points(Color c) const +{ + return m_starting_points[c]; +} + +inline bool StartingPoints::is_colored_starting_point(Point p) const +{ + return m_is_colored_starting_point[p]; +} + +inline bool StartingPoints::is_colorless_starting_point(Point p) const +{ + return m_is_colorless_starting_point[p]; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_STARTING_POINTS_H diff --git a/src/libpentobi_base/SymmetricPoints.cpp b/src/libpentobi_base/SymmetricPoints.cpp new file mode 100644 index 0000000..b392ad6 --- /dev/null +++ b/src/libpentobi_base/SymmetricPoints.cpp @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +/** @file SymmetricPoints.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "SymmetricPoints.h" + +#include "libboardgame_base/PointTransform.h" + +namespace libpentobi_base { + +using libboardgame_base::PointTransfRot180; + +//----------------------------------------------------------------------------- + +void SymmetricPoints::init(const Geometry& geo) +{ + PointTransfRot180 transform; + for (Point p : geo) + m_symmetric_point[p] = transform.get_transformed(p, geo); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + diff --git a/src/libpentobi_base/SymmetricPoints.h b/src/libpentobi_base/SymmetricPoints.h new file mode 100644 index 0000000..151452a --- /dev/null +++ b/src/libpentobi_base/SymmetricPoints.h @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/SymmetricPoints.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_SYMMETRIC_POINTS_H +#define LIBPENTOBI_BASE_SYMMETRIC_POINTS_H + +#include "Geometry.h" +#include "Grid.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +/** Lookup table to quickly get points that are symmetric with respect to the + center of the board. */ +class SymmetricPoints +{ +public: + void init(const Geometry& geo); + + Point operator[](Point p) const; + +private: + Grid m_symmetric_point; +}; + +inline Point SymmetricPoints::operator[](Point p) const +{ + return m_symmetric_point[p]; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_SYMMETRIC_POINTS_H diff --git a/src/libpentobi_base/TreeUtil.cpp b/src/libpentobi_base/TreeUtil.cpp new file mode 100644 index 0000000..8d38446 --- /dev/null +++ b/src/libpentobi_base/TreeUtil.cpp @@ -0,0 +1,74 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/TreeUtil.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "TreeUtil.h" + +#include "NodeUtil.h" +#include "libboardgame_sgf/SgfUtil.h" + +namespace libpentobi_base { +namespace tree_util { + +using libboardgame_sgf::util::get_move_annotation; +using libboardgame_sgf::util::get_variation_string; + +//----------------------------------------------------------------------------- + +unsigned get_move_number(const PentobiTree& tree, const SgfNode& node) +{ + unsigned move_number = 0; + auto current = &node; + while (current) + { + if (! tree.get_move_ignore_invalid(*current).is_null()) + ++move_number; + if (libpentobi_base::node_util::has_setup(*current)) + break; + current = current->get_parent_or_null(); + } + return move_number; +} + +unsigned get_moves_left(const PentobiTree& tree, const SgfNode& node) +{ + unsigned moves_left = 0; + auto current = node.get_first_child_or_null(); + while (current) + { + if (libpentobi_base::node_util::has_setup(*current)) + break; + if (! tree.get_move_ignore_invalid(*current).is_null()) + ++moves_left; + current = current->get_first_child_or_null(); + } + return moves_left; +} + +string get_position_info(const PentobiTree& tree, const SgfNode& node) +{ + auto move = get_move_number(tree, node); + auto left = get_moves_left(tree, node); + auto total = move + left; + auto variation = get_variation_string(node); + auto annotation = get_move_annotation(tree, node); + ostringstream s; + if (left > 0 || move > 0) + s << move << annotation; + if (left > 0) + s << '/' << total; + if (! variation.empty()) + s << " (" << variation << ')'; + return s.str(); +} + +//----------------------------------------------------------------------------- + +} // namespace tree_util +} // namespace libpentobi_base diff --git a/src/libpentobi_base/TreeUtil.h b/src/libpentobi_base/TreeUtil.h new file mode 100644 index 0000000..34171d1 --- /dev/null +++ b/src/libpentobi_base/TreeUtil.h @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/TreeUtil.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_TREE_UTIL_H +#define LIBPENTOBI_BASE_TREE_UTIL_H + +#include "PentobiTree.h" + +namespace libpentobi_base { +namespace tree_util { + +//----------------------------------------------------------------------------- + +/** Get the move number at a node. + Counts the number of moves since the root node or the last node + that contained setup properties. Invalid moves are ignored. */ +unsigned get_move_number(const PentobiTree& tree, const SgfNode& node); + +/** Get the number of remaining moves in the current variation. + Counts the number of moves remaining in the current variation + until the end of the variation or the next node that contains setup + properties. Invalid moves are ignored. */ +unsigned get_moves_left(const PentobiTree& tree, const SgfNode& node); + +/** Return a single line that describes the location of the current move + in the tree. + Includes the move number, move annotationm symbols, the total number of + moves in this variation, and a string describing the variation. */ +string get_position_info(const PentobiTree& tree, const SgfNode& node); + +//----------------------------------------------------------------------------- + +} // namespace tree_util +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_TREE_UTIL_H diff --git a/src/libpentobi_base/TrigonGeometry.cpp b/src/libpentobi_base/TrigonGeometry.cpp new file mode 100644 index 0000000..706980a --- /dev/null +++ b/src/libpentobi_base/TrigonGeometry.cpp @@ -0,0 +1,128 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/TrigonGeometry.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "TrigonGeometry.h" + +namespace libpentobi_base { + +using libboardgame_base::CoordPoint; + +//----------------------------------------------------------------------------- + +map> TrigonGeometry::s_geometry; + +TrigonGeometry::TrigonGeometry(unsigned sz) +{ + m_sz = sz; + Geometry::init(sz * 4 - 1, sz * 2); +} + +const TrigonGeometry& TrigonGeometry::get(unsigned sz) +{ + auto pos = s_geometry.find(sz); + if (pos != s_geometry.end()) + return *pos->second; + shared_ptr geometry(new TrigonGeometry(sz)); + return *s_geometry.insert(make_pair(sz, geometry)).first->second; +} + +auto TrigonGeometry::get_adj_coord(int x, int y) const -> AdjCoordList +{ + AdjCoordList l; + if (get_point_type(x, y) == 0) + { + l.push_back(CoordPoint(x - 1, y)); + l.push_back(CoordPoint(x + 1, y)); + l.push_back(CoordPoint(x, y + 1)); + } + else + { + l.push_back(CoordPoint(x, y - 1)); + l.push_back(CoordPoint(x - 1, y)); + l.push_back(CoordPoint(x + 1, y)); + } + return l; +} + +auto TrigonGeometry::get_diag_coord(int x, int y) const -> DiagCoordList +{ + // The order does not matter logically but it is better to put far away + // points first because BoardConst uses the forbidden status of the first + // points during move generation and far away points can reject more moves. + DiagCoordList l; + if (get_point_type(x, y) == 0) + { + l.push_back(CoordPoint(x - 2, y)); + l.push_back(CoordPoint(x + 2, y)); + l.push_back(CoordPoint(x - 1, y - 1)); + l.push_back(CoordPoint(x + 1, y - 1)); + l.push_back(CoordPoint(x + 1, y + 1)); + l.push_back(CoordPoint(x - 1, y + 1)); + l.push_back(CoordPoint(x, y - 1)); + l.push_back(CoordPoint(x - 2, y + 1)); + l.push_back(CoordPoint(x + 2, y + 1)); + } + else + { + l.push_back(CoordPoint(x - 2, y)); + l.push_back(CoordPoint(x + 2, y)); + l.push_back(CoordPoint(x - 1, y + 1)); + l.push_back(CoordPoint(x + 1, y + 1)); + l.push_back(CoordPoint(x + 1, y - 1)); + l.push_back(CoordPoint(x - 1, y - 1)); + l.push_back(CoordPoint(x, y + 1)); + l.push_back(CoordPoint(x - 2, y - 1)); + l.push_back(CoordPoint(x + 2, y - 1)); + } + return l; +} + +unsigned TrigonGeometry::get_period_x() const +{ + return 2; +} + +unsigned TrigonGeometry::get_period_y() const +{ + return 2; +} + +unsigned TrigonGeometry::get_point_type(int x, int y) const +{ + if (m_sz % 2 == 0) + { + if (x % 2 == 0) + return y % 2 == 0 ? 1 : 0; + else + return y % 2 != 0 ? 1 : 0; + } + else + { + if (x % 2 != 0) + return y % 2 == 0 ? 1 : 0; + else + return y % 2 != 0 ? 1 : 0; + } +} + +bool TrigonGeometry::init_is_onboard(unsigned x, unsigned y) const +{ + auto width = get_width(); + auto height = get_height(); + unsigned dy = min(y, height - y - 1); + unsigned min_x = m_sz - dy - 1; + unsigned max_x = width - min_x - 1; + return x >= min_x && x <= max_x; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + diff --git a/src/libpentobi_base/TrigonGeometry.h b/src/libpentobi_base/TrigonGeometry.h new file mode 100644 index 0000000..03118ad --- /dev/null +++ b/src/libpentobi_base/TrigonGeometry.h @@ -0,0 +1,69 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/TrigonGeometry.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_TRIGON_GEOMETRY_H +#define LIBPENTOBI_BASE_TRIGON_GEOMETRY_H + +#include +#include +#include "Geometry.h" + +namespace libpentobi_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Geometry as used in the game Blokus Trigon. + The board is a hexagon consisting of triangles. The coordinates are like + in this example of a hexagon with edge size 3: + + 0 1 2 3 4 5 6 7 8 9 10 + 0 / \ / \ / \ / \ + 1 / \ / \ / \ / \ / \ + 2 / \ / \ / \ / \ / \ / \ + 3 \ / \ / \ / \ / \ / \ / + 4 \ / \ / \ / \ / \ / + 5 \ / \ / \ / \ / + + There are two point types: 0=upward triangle, 1=downward triangle. */ +class TrigonGeometry final + : public Geometry +{ +public: + /** Create or reuse an already created geometry with a given size. + @param sz The edge size of the hexagon. */ + static const TrigonGeometry& get(unsigned sz); + + + AdjCoordList get_adj_coord(int x, int y) const override; + + DiagCoordList get_diag_coord(int x, int y) const override; + + unsigned get_point_type(int x, int y) const override; + + unsigned get_period_x() const override; + + unsigned get_period_y() const override; + +protected: + bool init_is_onboard(unsigned x, unsigned y) const override; + +private: + /** Stores already created geometries by size. */ + static map> s_geometry; + + unsigned m_sz; + + + explicit TrigonGeometry(unsigned sz); +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_TRIGON_GEOMETRY_H diff --git a/src/libpentobi_base/TrigonTransform.cpp b/src/libpentobi_base/TrigonTransform.cpp new file mode 100644 index 0000000..39a0675 --- /dev/null +++ b/src/libpentobi_base/TrigonTransform.cpp @@ -0,0 +1,135 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/TrigonTransform.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "TrigonTransform.h" + +#include + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +CoordPoint TransfTrigonIdentity::get_transformed(const CoordPoint& p) const +{ + return p; +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfTrigonRefl::get_transformed(const CoordPoint& p) const +{ + return CoordPoint(-p.x, p.y); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfTrigonRot60::get_transformed(const CoordPoint& p) const +{ + float px = static_cast(p.x); + float py = static_cast(p.y); + int x = static_cast(ceil(0.5f * px - 1.5f * py)); + int y = static_cast(floor(0.5f * px + 0.5f * py)); + return CoordPoint(x, y); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfTrigonRot120::get_transformed(const CoordPoint& p) const +{ + float px = static_cast(p.x); + float py = static_cast(p.y); + int x = static_cast(ceil(-0.5f * px - 1.5f * py)); + int y = static_cast(ceil(0.5f * px - 0.5f * py)); + return CoordPoint(x, y); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfTrigonRot180::get_transformed(const CoordPoint& p) const +{ + return CoordPoint(-p.x, -p.y); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfTrigonRot240::get_transformed(const CoordPoint& p) const +{ + float px = static_cast(p.x); + float py = static_cast(p.y); + int x = static_cast(floor(-0.5f * px + 1.5f * py)); + int y = static_cast(ceil(-0.5f * px - 0.5f * py)); + return CoordPoint(x, y); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfTrigonRot300::get_transformed(const CoordPoint& p) const +{ + float px = static_cast(p.x); + float py = static_cast(p.y); + int x = static_cast(floor(0.5f * px + 1.5f * py)); + int y = static_cast(floor(-0.5f * px + 0.5f * py)); + return CoordPoint(x, y); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfTrigonReflRot60::get_transformed(const CoordPoint& p) const +{ + float px = static_cast(p.x); + float py = static_cast(p.y); + int x = static_cast(ceil(0.5f * (-px) - 1.5f * py)); + int y = static_cast(floor(0.5f * (-px) + 0.5f * py)); + return CoordPoint(x, y); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfTrigonReflRot120::get_transformed(const CoordPoint& p) const +{ + float px = static_cast(p.x); + float py = static_cast(p.y); + int x = static_cast(ceil(-0.5f * (-px) - 1.5f * py)); + int y = static_cast(ceil(0.5f * (-px) - 0.5f * py)); + return CoordPoint(x, y); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfTrigonReflRot180::get_transformed(const CoordPoint& p) const +{ + return CoordPoint(p.x, -p.y); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfTrigonReflRot240::get_transformed(const CoordPoint& p) const +{ + float px = static_cast(p.x); + float py = static_cast(p.y); + int x = static_cast(floor(-0.5f * (-px) + 1.5f * py)); + int y = static_cast(ceil(-0.5f * (-px) - 0.5f * py)); + return CoordPoint(x, y); +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfTrigonReflRot300::get_transformed(const CoordPoint& p) const +{ + float px = static_cast(p.x); + float py = static_cast(p.y); + int x = static_cast(floor(0.5f * (-px) + 1.5f * py)); + int y = static_cast(floor(-0.5f * (-px) + 0.5f * py)); + return CoordPoint(x, y); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/TrigonTransform.h b/src/libpentobi_base/TrigonTransform.h new file mode 100644 index 0000000..4e33871 --- /dev/null +++ b/src/libpentobi_base/TrigonTransform.h @@ -0,0 +1,153 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/TrigonTransform.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_TRIGON_TRANSFORM_H +#define LIBPENTOBI_BASE_TRIGON_TRANSFORM_H + +#include "libboardgame_base/Transform.h" + +namespace libpentobi_base { + +using libboardgame_base::CoordPoint; +using libboardgame_base::Transform; + +//----------------------------------------------------------------------------- + +class TransfTrigonIdentity + : public Transform +{ +public: + TransfTrigonIdentity() : Transform(0) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfTrigonRot60 + : public Transform +{ +public: + TransfTrigonRot60() : Transform(1) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfTrigonRot120 + : public Transform +{ +public: + TransfTrigonRot120() : Transform(0) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfTrigonRot180 + : public Transform +{ +public: + TransfTrigonRot180() : Transform(1) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfTrigonRot240 + : public Transform +{ +public: + TransfTrigonRot240() : Transform(0) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfTrigonRot300 + : public Transform +{ +public: + TransfTrigonRot300() : Transform(1) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfTrigonRefl + : public Transform +{ +public: + TransfTrigonRefl() : Transform(0) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfTrigonReflRot60 + : public Transform +{ +public: + TransfTrigonReflRot60() : Transform(1) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfTrigonReflRot120 + : public Transform +{ +public: + TransfTrigonReflRot120() : Transform(0) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfTrigonReflRot180 + : public Transform +{ +public: + TransfTrigonReflRot180() : Transform(1) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfTrigonReflRot240 + : public Transform +{ +public: + TransfTrigonReflRot240() : Transform(0) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +class TransfTrigonReflRot300 + : public Transform +{ +public: + TransfTrigonReflRot300() : Transform(1) {} + + CoordPoint get_transformed(const CoordPoint& p) const override; +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_TRIGON_TRANSFORM_H diff --git a/src/libpentobi_base/Variant.cpp b/src/libpentobi_base/Variant.cpp new file mode 100644 index 0000000..c0ee1f7 --- /dev/null +++ b/src/libpentobi_base/Variant.cpp @@ -0,0 +1,442 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Variant.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Variant.h" + +#include "CallistoGeometry.h" +#include "NexosGeometry.h" +#include "TrigonGeometry.h" +#include "libboardgame_base/RectGeometry.h" +#include "libboardgame_util/StringUtil.h" + +namespace libpentobi_base { + +using libboardgame_base::PointTransfIdent; +using libboardgame_base::PointTransfRefl; +using libboardgame_base::PointTransfReflRot180; +using libboardgame_base::PointTransfRot90; +using libboardgame_base::PointTransfRot180; +using libboardgame_base::PointTransfRot270; +using libboardgame_base::PointTransfRot90Refl; +using libboardgame_base::PointTransfRot270Refl; +using libboardgame_base::PointTransfTrigonReflRot60; +using libboardgame_base::PointTransfTrigonReflRot120; +using libboardgame_base::PointTransfTrigonReflRot240; +using libboardgame_base::PointTransfTrigonReflRot300; +using libboardgame_base::PointTransfTrigonRot60; +using libboardgame_base::PointTransfTrigonRot120; +using libboardgame_base::PointTransfTrigonRot240; +using libboardgame_base::PointTransfTrigonRot300; +using libboardgame_base::RectGeometry; +using libboardgame_util::trim; +using libboardgame_util::to_lower; + +//----------------------------------------------------------------------------- + +BoardType get_board_type(Variant variant) +{ + BoardType result = BoardType::classic; // Init to avoid compiler warning + switch (variant) + { + case Variant::duo: + case Variant::junior: + result = BoardType::duo; + break; + case Variant::classic: + case Variant::classic_2: + case Variant::classic_3: + result = BoardType::classic; + break; + case Variant::trigon: + case Variant::trigon_2: + result = BoardType::trigon; + break; + case Variant::trigon_3: + result = BoardType::trigon_3; + break; + case Variant::nexos: + case Variant::nexos_2: + result = BoardType::nexos; + break; + case Variant::callisto: + result = BoardType::callisto; + break; + case Variant::callisto_2: + result = BoardType::callisto_2; + break; + case Variant::callisto_3: + result = BoardType::callisto_3; + break; + } + return result; +} + +const Geometry& get_geometry(BoardType board_type) +{ + const Geometry* result = nullptr; // Init to avoid compiler warning + switch (board_type) + { + case BoardType::duo: + result = &RectGeometry::get(14, 14); + break; + case BoardType::classic: + result = &RectGeometry::get(20, 20); + break; + case BoardType::trigon: + result = &TrigonGeometry::get(9); + break; + case BoardType::trigon_3: + result = &TrigonGeometry::get(8); + break; + case BoardType::nexos: + result = &NexosGeometry::get(13); + break; + case BoardType::callisto: + result = &CallistoGeometry::get(4); + break; + case BoardType::callisto_2: + result = &CallistoGeometry::get(2); + break; + case BoardType::callisto_3: + result = &CallistoGeometry::get(3); + break; + } + return *result; +} + +const Geometry& get_geometry(Variant variant) +{ + return get_geometry(get_board_type(variant)); +} + +Color::IntType get_nu_colors(Variant variant) +{ + Color::IntType result = 0; // Init to avoid compiler warning + switch (variant) + { + case Variant::duo: + case Variant::junior: + case Variant::callisto_2: + result = 2; + break; + case Variant::trigon_3: + case Variant::callisto_3: + result = 3; + break; + case Variant::classic: + case Variant::classic_2: + case Variant::classic_3: + case Variant::trigon: + case Variant::trigon_2: + case Variant::nexos: + case Variant::nexos_2: + case Variant::callisto: + result = 4; + break; + } + return result; +} + +Color::IntType get_nu_players(Variant variant) +{ + Color::IntType result = 0; // Init to avoid compiler warning + switch (variant) + { + case Variant::duo: + case Variant::junior: + case Variant::classic_2: + case Variant::trigon_2: + case Variant::nexos_2: + case Variant::callisto_2: + result = 2; + break; + case Variant::classic_3: + case Variant::trigon_3: + case Variant::callisto_3: + result = 3; + break; + case Variant::classic: + case Variant::trigon: + case Variant::nexos: + case Variant::callisto: + result = 4; + break; + } + return result; +} + +PieceSet get_piece_set(Variant variant) +{ + PieceSet result = PieceSet::classic; // Init to avoid compiler warning + switch (variant) + { + case Variant::classic: + case Variant::classic_2: + case Variant::classic_3: + case Variant::duo: + result = PieceSet::classic; + break; + case Variant::trigon: + case Variant::trigon_2: + case Variant::trigon_3: + result = PieceSet::trigon; + break; + case Variant::junior: + result = PieceSet::junior; + break; + case Variant::nexos: + case Variant::nexos_2: + result = PieceSet::nexos; + break; + case Variant::callisto: + case Variant::callisto_2: + case Variant::callisto_3: + result = PieceSet::callisto; + break; + } + return result; +} + +void get_transforms(Variant variant, + vector>>& transforms, + vector>>& inv_transforms) +{ + transforms.clear(); + inv_transforms.clear(); + transforms.emplace_back(new PointTransfIdent); + inv_transforms.emplace_back(new PointTransfIdent); + switch (get_board_type(variant)) + { + case BoardType::duo: + transforms.emplace_back(new PointTransfRot270Refl); + inv_transforms.emplace_back(new PointTransfRot270Refl); + break; + case BoardType::trigon: + transforms.emplace_back(new PointTransfTrigonRot60); + inv_transforms.emplace_back(new PointTransfTrigonRot300); + transforms.emplace_back(new PointTransfTrigonRot120); + inv_transforms.emplace_back(new PointTransfTrigonRot240); + transforms.emplace_back(new PointTransfRot180); + inv_transforms.emplace_back(new PointTransfRot180); + transforms.emplace_back(new PointTransfTrigonRot240); + inv_transforms.emplace_back(new PointTransfTrigonRot120); + transforms.emplace_back(new PointTransfTrigonRot300); + inv_transforms.emplace_back(new PointTransfTrigonRot60); + transforms.emplace_back(new PointTransfRefl); + inv_transforms.emplace_back(new PointTransfRefl); + transforms.emplace_back(new PointTransfTrigonReflRot60); + inv_transforms.emplace_back(new PointTransfTrigonReflRot60); + transforms.emplace_back(new PointTransfTrigonReflRot120); + inv_transforms.emplace_back(new PointTransfTrigonReflRot120); + transforms.emplace_back(new PointTransfReflRot180); + inv_transforms.emplace_back(new PointTransfReflRot180); + transforms.emplace_back(new PointTransfTrigonReflRot240); + inv_transforms.emplace_back(new PointTransfTrigonReflRot240); + transforms.emplace_back(new PointTransfTrigonReflRot300); + inv_transforms.emplace_back(new PointTransfTrigonReflRot300); + break; + case BoardType::callisto_2: + case BoardType::callisto: + case BoardType::callisto_3: + transforms.emplace_back(new PointTransfRot90); + inv_transforms.emplace_back(new PointTransfRot270); + transforms.emplace_back(new PointTransfRot180); + inv_transforms.emplace_back(new PointTransfRot180); + transforms.emplace_back(new PointTransfRot270); + inv_transforms.emplace_back(new PointTransfRot90); + transforms.emplace_back(new PointTransfRefl); + inv_transforms.emplace_back(new PointTransfRefl); + transforms.emplace_back(new PointTransfReflRot180); + inv_transforms.emplace_back(new PointTransfReflRot180); + transforms.emplace_back(new PointTransfRot90Refl); + inv_transforms.emplace_back(new PointTransfRot90Refl); + transforms.emplace_back(new PointTransfRot270Refl); + inv_transforms.emplace_back(new PointTransfRot270Refl); + break; + case BoardType::classic: + case BoardType::trigon_3: + case BoardType::nexos: + break; + } +} + +bool has_central_symmetry(Variant variant) +{ + return variant == Variant::duo || variant == Variant::junior + || variant == Variant::trigon_2 || variant == Variant::callisto_2; +} + +bool parse_variant(const string& s, Variant& variant) +{ + string t = to_lower(trim(s)); + if (t == "blokus") + variant = Variant::classic; + else if (t == "blokus two-player") + variant = Variant::classic_2; + else if (t == "blokus three-player") + variant = Variant::classic_3; + else if (t == "blokus trigon") + variant = Variant::trigon; + else if (t == "blokus trigon two-player") + variant = Variant::trigon_2; + else if (t == "blokus trigon three-player") + variant = Variant::trigon_3; + else if (t == "blokus duo") + variant = Variant::duo; + else if (t == "blokus junior") + variant = Variant::junior; + else if (t == "nexos") + variant = Variant::nexos; + else if (t == "nexos two-player") + variant = Variant::nexos_2; + else if (t == "callisto") + variant = Variant::callisto; + else if (t == "callisto two-player") + variant = Variant::callisto_2; + else if (t == "callisto three-player") + variant = Variant::callisto_3; + else + return false; + return true; +} + +bool parse_variant_id(const string& s, Variant& variant) +{ + string t = to_lower(trim(s)); + if (t == "classic" || t == "c") + variant = Variant::classic; + else if (t == "classic_2" || t == "c2") + variant = Variant::classic_2; + else if (t == "classic_3" || t == "c3") + variant = Variant::classic_3; + else if (t == "trigon" || t == "t") + variant = Variant::trigon; + else if (t == "trigon_2" || t == "t2") + variant = Variant::trigon_2; + else if (t == "trigon_3" || t == "t3") + variant = Variant::trigon_3; + else if (t == "duo" || t == "d") + variant = Variant::duo; + else if (t == "junior" || t == "j") + variant = Variant::junior; + else if (t == "nexos" || t == "n") + variant = Variant::nexos; + else if (t == "nexos_2" || t == "n2") + variant = Variant::nexos_2; + else if (t == "callisto" || t == "ca") + variant = Variant::callisto; + else if (t == "callisto_2" || t == "ca2") + variant = Variant::callisto_2; + else if (t == "callisto_3" || t == "ca3") + variant = Variant::callisto_3; + else + return false; + return true; +} + +const char* to_string(Variant variant) +{ + const char* result = nullptr; // Init to avoid compiler warning + switch (variant) + { + case Variant::classic: + result = "Blokus"; + break; + case Variant::classic_2: + result = "Blokus Two-Player"; + break; + case Variant::classic_3: + result = "Blokus Three-Player"; + break; + case Variant::duo: + result = "Blokus Duo"; + break; + case Variant::junior: + result = "Blokus Junior"; + break; + case Variant::trigon: + result = "Blokus Trigon"; + break; + case Variant::trigon_2: + result = "Blokus Trigon Two-Player"; + break; + case Variant::trigon_3: + result = "Blokus Trigon Three-Player"; + break; + case Variant::nexos: + result = "Nexos"; + break; + case Variant::nexos_2: + result = "Nexos Two-Player"; + break; + case Variant::callisto: + result = "Callisto"; + break; + case Variant::callisto_2: + result = "Callisto Two-Player"; + break; + case Variant::callisto_3: + result = "Callisto Three-Player"; + break; + } + return result; +} + +const char* to_string_id(Variant variant) +{ + const char* result = nullptr; // Init to avoid compiler warning + switch (variant) + { + case Variant::classic: + result = "classic"; + break; + case Variant::classic_2: + result = "classic_2"; + break; + case Variant::classic_3: + result = "classic_3"; + break; + case Variant::duo: + result = "duo"; + break; + case Variant::junior: + result = "junior"; + break; + case Variant::trigon: + result = "trigon"; + break; + case Variant::trigon_2: + result = "trigon_2"; + break; + case Variant::trigon_3: + result = "trigon_3"; + break; + case Variant::nexos: + result = "nexos"; + break; + case Variant::nexos_2: + result = "nexos_2"; + break; + case Variant::callisto: + result = "callisto"; + break; + case Variant::callisto_2: + result = "callisto_2"; + break; + case Variant::callisto_3: + result = "callisto_3"; + break; + } + return result; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/Variant.h b/src/libpentobi_base/Variant.h new file mode 100644 index 0000000..dcf8a94 --- /dev/null +++ b/src/libpentobi_base/Variant.h @@ -0,0 +1,150 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Variant.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_BASE_VARIANT_H +#define LIBPENTOBI_BASE_VARIANT_H + +#include +#include +#include +#include "Color.h" +#include "Geometry.h" +#include "libboardgame_base/PointTransform.h" + +namespace libpentobi_base { + +using libboardgame_base::PointTransform; + +//----------------------------------------------------------------------------- + +enum class PieceSet +{ + classic, + + junior, + + trigon, + + nexos, + + callisto +}; + +//----------------------------------------------------------------------------- + +enum class BoardType +{ + classic, + + duo, + + trigon, + + trigon_3, + + nexos, + + callisto, + + callisto_2, + + callisto_3, +}; + +//----------------------------------------------------------------------------- + +/** Game variant. */ +enum class Variant +{ + classic, + + classic_2, + + classic_3, + + duo, + + junior, + + trigon, + + trigon_2, + + trigon_3, + + nexos, + + nexos_2, + + callisto, + + callisto_2, + + callisto_3 +}; + +//----------------------------------------------------------------------------- + +/** Get name of game variant as in the GM property in Blokus SGF files. */ +const char* to_string(Variant variant); + +/** Get a short lowercase string without spaces that can be used as + a identifier for a game variant. + The strings used are "classic", "classic_2", "duo", "trigon", "trigon_2", + "trigon_3", "junior" */ +const char* to_string_id(Variant variant); + +/** Parse name of game variant as in the GM property in Blokus SGF files. + The parsing is case-insensitive, leading and trailing whitespaced are + ignored. + @param s + @param[out] variant + @result True if the string contained a valid game variant. */ +bool parse_variant(const string& s, Variant& variant); + +/** Parse short lowercase name of game variant as returned to_string_id(). + @param s + @param[out] variant + @result True if the string contained a valid game variant. */ +bool parse_variant_id(const string& s, Variant& variant); + +Color::IntType get_nu_colors(Variant variant); + +inline Color::Range get_colors(Variant variant) +{ + return Color::Range(get_nu_colors(variant)); +} + +Color::IntType get_nu_players(Variant variant); + +const Geometry& get_geometry(BoardType board_type); + +const Geometry& get_geometry(Variant variant); + +BoardType get_board_type(Variant variant); + +PieceSet get_piece_set(Variant variant); + +/** Get invariance transformations for a game variant. + The invariance transformations depend on the symmetry of the board type and + the starting points. + @param variant The game variant. + @param[out] transforms The invariance transformations. + @param[out] inv_transforms The inverse transformations of the elements in + transforms. */ +void get_transforms(Variant variant, + vector>>& transforms, + vector>>& inv_transforms); + +/** Is the variant a two-player variant with the board including the starting + points invariant through point reflection through its center? */ +bool has_central_symmetry(Variant variant); + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_VARIANT_H diff --git a/src/libpentobi_gui/BoardPainter.cpp b/src/libpentobi_gui/BoardPainter.cpp new file mode 100644 index 0000000..2c8c6de --- /dev/null +++ b/src/libpentobi_gui/BoardPainter.cpp @@ -0,0 +1,620 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/BoardPainter.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "BoardPainter.h" +#include "libpentobi_base/CallistoGeometry.h" + +#include +#include +#include "Util.h" + +using namespace std; +using libboardgame_util::ArrayList; +using libpentobi_base::BoardType; +using libpentobi_base::CallistoGeometry; +using libpentobi_base::Move; +using libpentobi_base::PieceSet; +using libpentobi_base::PointState; + +//----------------------------------------------------------------------------- + +BoardPainter::BoardPainter() +{ + m_font.setFamily("Helvetica"); + m_font.setStyleHint(QFont::SansSerif); + m_font.setStyleStrategy(QFont::PreferOutline); + m_fontSemiCondensed = m_font; + m_fontSemiCondensed.setStretch(QFont::SemiCondensed); + m_fontCondensed = m_font; + m_fontCondensed.setStretch(QFont::Condensed); + m_fontCoordLabels = m_font; + m_fontCoordLabels.setStretch(QFont::SemiCondensed); +} + +BoardPainter::~BoardPainter() = default; + +CoordPoint BoardPainter::getCoordPoint(int x, int y) +{ + if (! m_hasPainted) + return CoordPoint::null(); + x = static_cast((x - m_boardOffset.x()) / m_fieldWidth); + y = static_cast((y - m_boardOffset.y()) / m_fieldHeight); + if (x < 0 || x >= m_width || y < 0 || y >= m_height) + return CoordPoint::null(); + else + return CoordPoint(x, y); +} + +void BoardPainter::paintCoordinates(QPainter& painter) +{ + painter.setPen(m_coordinateColor); + for (int x = 0; x < m_width; ++x) + { + QString label; + if (x < 26) + label = QString(QChar('A' + x)); + else + { + label = "A"; + label.append(QChar('A' + (x - 26))); + } + paintLabel(painter, x * m_fieldWidth, m_height * m_fieldHeight, + m_fieldWidth, m_fieldHeight, label, true); + paintLabel(painter, x * m_fieldWidth, -m_fieldHeight, + m_fieldWidth, m_fieldHeight, label, true); + } + for (int y = 0; y < m_height; ++y) + { + QString label; + label.setNum(y + 1); + qreal left; + qreal right; + if (m_isTrigon) + { + left = -1.5 * m_fieldWidth; + right = (m_width + 0.5) * m_fieldWidth; + } + else + { + left = -m_fieldWidth; + right = m_width * m_fieldWidth; + } + paintLabel(painter, left, (m_height - y - 1) * m_fieldHeight, + m_fieldWidth, m_fieldHeight, label, true); + paintLabel(painter, right, (m_height - y - 1) * m_fieldHeight, + m_fieldWidth, m_fieldHeight, label, true); + } +} + +void BoardPainter::paintEmptyBoard(QPainter& painter, unsigned width, + unsigned height, Variant variant, + const Geometry& geo) +{ + m_hasPainted = true; + painter.setRenderHint(QPainter::Antialiasing, true); + m_variant = variant; + auto pieceSet = get_piece_set(variant); + m_geo = &geo; + m_width = static_cast(m_geo->get_width()); + m_height = static_cast(m_geo->get_height()); + m_isTrigon = (pieceSet == PieceSet::trigon); + m_isNexos = (pieceSet == PieceSet::nexos); + m_isCallisto = (pieceSet == PieceSet::callisto); + qreal ratio; + if (m_isTrigon) + { + ratio = 1.732; + if (m_coordinates) + m_fieldWidth = + min(qreal(width) / (m_width + 3), + height / (ratio * (m_height + 2))); + else + m_fieldWidth = + min(qreal(width) / (m_width + 1), height / (ratio * m_height)); + } + else + { + ratio = 1; + if (m_coordinates) + m_fieldWidth = + min(qreal(width) / (m_width + 2), + qreal(height) / (m_height + 2)); + else + m_fieldWidth = + min(qreal(width) / m_width, qreal(height) / m_height); + } + if (m_fieldWidth > 8) + // Prefer pixel alignment if board is not too small + m_fieldWidth = floor(m_fieldWidth); + m_fieldHeight = ratio * m_fieldWidth; + m_boardOffset = QPointF(0.5 * (width - m_fieldWidth * m_width), + 0.5 * (height - m_fieldHeight * m_height)); + // QFont::setPixelSize(0) prints a warning even if it works and the docs + // of Qt 5.3 don't forbid it (unlike QFont::setPointSize(0)). + int fontSize = + max(1, static_cast((m_isTrigon ? 0.7 : 0.5) * m_fieldWidth)); + m_font.setPixelSize(fontSize); + m_fontSemiCondensed.setPixelSize(fontSize); + m_fontCondensed.setPixelSize(fontSize); + m_fontCoordLabels.setPixelSize(fontSize); + painter.save(); + painter.translate(m_boardOffset); + if (m_coordinates) + paintCoordinates(painter); + if (m_isNexos) + painter.fillRect(QRectF(m_fieldWidth / 4, m_fieldHeight / 4, + m_width * m_fieldWidth - m_fieldWidth / 2, + m_height * m_fieldHeight - m_fieldHeight / 2), + QColor(174, 167, 172)); + auto nu_players = get_nu_players(m_variant); + for (Point p : *m_geo) + { + int x = m_geo->get_x(p); + int y = m_geo->get_y(p); + qreal fieldX = x * m_fieldWidth; + qreal fieldY = y * m_fieldHeight; + auto pointType = m_geo->get_point_type(p); + if (m_isTrigon) + { + bool isUpward = (pointType == 0); + Util::paintEmptyTriangle(painter, isUpward, fieldX, fieldY, + m_fieldWidth, m_fieldHeight); + } + else if (m_isNexos) + { + if (pointType == 1 || pointType == 2) + { + bool isHorizontal = (pointType == 1); + Util::paintEmptySegment(painter, isHorizontal, fieldX, fieldY, + m_fieldWidth); + } + else + { + LIBBOARDGAME_ASSERT(pointType == 0); + Util::paintEmptyJunction(painter, fieldX, fieldY, + m_fieldWidth); + } + } + else if (m_isCallisto + && CallistoGeometry::is_center_section(x, y, nu_players)) + Util::paintEmptySquareCallistoCenter(painter, fieldX, fieldY, + m_fieldWidth); + else if (m_isCallisto) + Util::paintEmptySquareCallisto(painter, fieldX, fieldY, + m_fieldWidth); + else + Util::paintEmptySquare(painter, fieldX, fieldY, m_fieldWidth); + } + painter.restore(); +} + +void BoardPainter::paintJunction(QPainter& painter, Variant variant, + const Grid& pointState, + const Grid& pieceId, int x, int y, + qreal fieldX, qreal fieldY) +{ + LIBBOARDGAME_ASSERT(m_geo->get_point_type(x, y) == 0); + ArrayList pieces; + if (x > 0) + { + auto piece = pieceId[m_geo->get_point(x - 1, y)]; + if (piece != 0) + pieces.include(piece); + } + if (x < m_width - 1) + { + auto piece = pieceId[m_geo->get_point(x + 1, y)]; + if (piece != 0) + pieces.include(piece); + } + if (y > 0) + { + auto piece = pieceId[m_geo->get_point(x, y - 1)]; + if (piece != 0) + pieces.include(piece); + } + if (y < m_height - 1) + { + auto piece = pieceId[m_geo->get_point(x, y + 1)]; + if (piece != 0) + pieces.include(piece); + } + for (auto piece : pieces) + { + Color c; + bool hasLeft = false; + if (x > 0) + { + Point p = m_geo->get_point(x - 1, y); + if (pieceId[p] == piece) + { + hasLeft = true; + c = pointState[p].to_color(); + } + } + bool hasRight = false; + if (x < m_width - 1) + { + Point p = m_geo->get_point(x + 1, y); + if (pieceId[p] == piece) + { + hasRight = true; + c = pointState[p].to_color(); + } + } + bool hasUp = false; + if (y > 0) + { + Point p = m_geo->get_point(x, y - 1); + if (pieceId[p] == piece) + { + hasUp = true; + c = pointState[p].to_color(); + } + } + bool hasDown = false; + if (y < m_height - 1) + { + Point p = m_geo->get_point(x, y + 1); + if (pieceId[p] == piece) + { + hasDown = true; + c = pointState[p].to_color(); + } + } + Util::paintJunction(painter, variant, c, fieldX, fieldY, m_fieldWidth, + m_fieldHeight, hasLeft, hasRight, hasUp, hasDown); + } +} + +void BoardPainter::paintLabel(QPainter& painter, qreal x, qreal y, + qreal width, qreal height, const QString& label, + bool isCoordLabel) +{ + if (isCoordLabel) + painter.setFont(m_fontCoordLabels); + else + painter.setFont(m_font); + QFontMetrics metrics(painter.font()); + QRect boundingRect = metrics.boundingRect(label); + if (! isCoordLabel) + { + if (boundingRect.width() > width) + { + painter.setFont(m_fontSemiCondensed); + QFontMetrics metrics(painter.font()); + boundingRect = metrics.boundingRect(label); + } + if (boundingRect.width() > width) + { + painter.setFont(m_fontCondensed); + QFontMetrics metrics(painter.font()); + boundingRect = metrics.boundingRect(label); + } + } + qreal dx = 0.5 * (width - boundingRect.width()); + qreal dy = 0.5 * (height - boundingRect.height()); + QRectF rect; + rect.setCoords(floor(x + dx), floor(y + dy), + ceil(x + width - dx + 1), ceil(y + height - dy + 1)); + painter.drawText(rect, Qt::TextDontClip, label); +} + +void BoardPainter::paintLabels(QPainter& painter, + const Grid& pointState, + Variant variant, const Grid& labels) +{ + for (Point p : *m_geo) + if (! labels[p].isEmpty()) + { + painter.setPen(Util::getLabelColor(variant, pointState[p])); + qreal x = m_geo->get_x(p) * m_fieldWidth; + qreal y = m_geo->get_y(p) * m_fieldHeight; + qreal width = m_fieldWidth; + qreal height = m_fieldHeight; + if (m_isTrigon) + { + bool isUpward = (m_geo->get_point_type(p) == 0); + if (isUpward) + y += 0.333 * height; + height = 0.666 * height; + } + paintLabel(painter, x, y, width, height, labels[p], false); + } +} + +void BoardPainter::paintMarks(QPainter& painter, + const Grid& pointState, + Variant variant, const Grid& marks) +{ + for (Point p : *m_geo) + if (marks[p] & (dot | circle)) + { + qreal x = (static_cast(m_geo->get_x(p)) + 0.5f) + * m_fieldWidth; + qreal y = (static_cast(m_geo->get_y(p)) + 0.5f) + * m_fieldHeight; + qreal size; + if (m_isTrigon) + { + bool isUpward = (m_geo->get_point_type(p) == 0); + if (isUpward) + y += 0.167 * m_fieldHeight; + else + y -= 0.167 * m_fieldHeight; + size = 0.1 * m_fieldHeight; + } + else if (m_isCallisto) + size = 0.1 * m_fieldHeight; + else + size = 0.12 * m_fieldHeight; + QColor color = Util::getMarkColor(variant, pointState[p]); + qreal penWidth = 0.05 * m_fieldHeight; + if (marks[p] & dot) + { + color.setAlphaF(0.5); + painter.setPen(Qt::NoPen); + painter.setBrush(color); + size *= (1 + 0.25 * penWidth); + } + else + { + color.setAlphaF(0.6); + QPen pen(color); + pen.setWidthF(penWidth); + painter.setPen(pen); + painter.setBrush(Qt::NoBrush); + } + painter.drawEllipse(QPointF(x, y), size, size); + } +} + +void BoardPainter::paintPieces(QPainter& painter, + const Grid& pointState, + const Grid& pieceId, + const Grid* labels, + const Grid* marks) +{ + painter.setRenderHint(QPainter::Antialiasing, true); + painter.save(); + painter.translate(m_boardOffset); + ColorMap isFirstPiece(true); + for (Point p : *m_geo) + { + int x = m_geo->get_x(p); + int y = m_geo->get_y(p); + PointState s = pointState[p]; + qreal fieldX = x * m_fieldWidth; + qreal fieldY = y * m_fieldHeight; + auto pointType = m_geo->get_point_type(p); + if (m_isTrigon) + { + if (s.is_empty()) + continue; + Color c = s.to_color(); + isFirstPiece[c] = false; + bool isUpward = (pointType == 0); + Util::paintColorTriangle(painter, m_variant, c, isUpward, fieldX, + fieldY, m_fieldWidth, m_fieldHeight); + } + else if (m_isNexos) + { + if (pointType == 1 || pointType == 2) + { + if (s.is_empty()) + continue; + Color c = s.to_color(); + isFirstPiece[c] = false; + bool isHorizontal = (pointType == 1); + Util::paintColorSegment(painter, m_variant, c, isHorizontal, + fieldX, fieldY, m_fieldWidth); + } + else + { + LIBBOARDGAME_ASSERT(pointType == 0); + paintJunction(painter, m_variant, pointState, pieceId, x, y, + fieldX, fieldY); + } + } + else + { + if (s.is_empty()) + continue; + Color c = s.to_color(); + isFirstPiece[c] = false; + if (m_isCallisto) + { + bool hasLeft = + (x > 0 && m_geo->is_onboard(x - 1, y) + && pieceId[p] == pieceId[m_geo->get_point(x - 1, y)]); + bool hasRight = + (x < m_width - 1 && m_geo->is_onboard(x + 1, y) + && pieceId[p] == pieceId[m_geo->get_point(x + 1, y)]); + bool hasUp = + (y > 0 && m_geo->is_onboard(x, y - 1) + && pieceId[p] == pieceId[m_geo->get_point(x, y - 1)]); + bool hasDown = + (y < m_height - 1 && m_geo->is_onboard(x, y + 1) + && pieceId[p] == pieceId[m_geo->get_point(x, y + 1)]); + bool isOnePiece = + (! hasLeft && ! hasRight && ! hasUp && ! hasDown); + Util::paintColorSquareCallisto(painter, m_variant, c, fieldX, + fieldY, m_fieldWidth, hasRight, + hasDown, isOnePiece); + } + else + Util::paintColorSquare(painter, m_variant, c, fieldX, fieldY, + m_fieldWidth); + } + } + paintStartingPoints(painter, m_variant, pointState, isFirstPiece); + if (marks) + paintMarks(painter, pointState, m_variant, *marks); + if (labels) + paintLabels(painter, pointState, m_variant, *labels); + painter.restore(); +} + +void BoardPainter::paintSelectedPiece(QPainter& painter, Color c, + const MovePoints& points, bool isLegal) +{ + painter.setRenderHint(QPainter::Antialiasing, true); + painter.save(); + painter.translate(m_boardOffset); + qreal alpha; + qreal saturation; + bool flat; + if (isLegal) + { + alpha = 0.9; + saturation = 0.8; + flat = false; + } + else + { + alpha = 0.63; + saturation = 0.5; + flat = true; + } + ArrayList junctions; + for (Point p : points) + { + if (p.is_null()) + continue; + auto x = m_geo->get_x(p); + auto y = m_geo->get_y(p); + auto pointType = m_geo->get_point_type(p); + qreal fieldX = x * m_fieldWidth; + qreal fieldY = y * m_fieldHeight; + if (m_isTrigon) + { + bool isUpward = (pointType == 0); + Util::paintColorTriangle(painter, m_variant, c, isUpward, + fieldX, fieldY, m_fieldWidth, + m_fieldHeight, alpha, saturation, flat); + } + else if (m_isNexos) + { + if (pointType == 1 || pointType == 2) + { + bool isHorizontal = (pointType == 1); + Util::paintColorSegment(painter, m_variant, c, isHorizontal, + fieldX, fieldY, m_fieldWidth, alpha, + saturation, flat); + if (isHorizontal) + { + if (m_geo->is_onboard(x - 1, y)) + junctions.include(m_geo->get_point(x - 1, y)); + if (m_geo->is_onboard(x + 1, y)) + junctions.include(m_geo->get_point(x + 1, y)); + } + else + { + if (m_geo->is_onboard(x, y - 1)) + junctions.include(m_geo->get_point(x, y - 1)); + if (m_geo->is_onboard(x, y + 1)) + junctions.include(m_geo->get_point(x, y + 1)); + } + } + } + else if (m_isCallisto) + { + bool hasRight = (m_geo->is_onboard(CoordPoint(x + 1, y)) + && points.contains(m_geo->get_point(x + 1, y))); + bool hasDown = (m_geo->is_onboard(CoordPoint(x, y + 1)) + && points.contains(m_geo->get_point(x, y + 1))); + bool isOnePiece = (points.size() == 1); + Util::paintColorSquareCallisto(painter, m_variant, c, fieldX, + fieldY, m_fieldWidth, hasRight, + hasDown, isOnePiece, alpha, + saturation, flat); + } + else + Util::paintColorSquare(painter, m_variant, c, fieldX, fieldY, + m_fieldWidth, alpha, saturation, flat); + } + if (m_isNexos) + for (auto p : junctions) + { + auto x = m_geo->get_x(p); + auto y = m_geo->get_y(p); + bool hasLeft = (m_geo->is_onboard(CoordPoint(x - 1, y)) + && points.contains(m_geo->get_point(x - 1, y))); + bool hasRight = (m_geo->is_onboard(CoordPoint(x + 1, y)) + && points.contains(m_geo->get_point(x + 1, y))); + bool hasUp = (m_geo->is_onboard(CoordPoint(x, y - 1)) + && points.contains(m_geo->get_point(x, y - 1))); + bool hasDown = (m_geo->is_onboard(CoordPoint(x, y + 1)) + && points.contains(m_geo->get_point(x, y + 1))); + Util::paintJunction(painter, m_variant, c, x * m_fieldWidth, + y * m_fieldHeight, m_fieldWidth, m_fieldHeight, + hasLeft, hasRight, hasUp, hasDown, alpha, + saturation); + } + painter.restore(); +} + +void BoardPainter::paintStartingPoints(QPainter& painter, Variant variant, + const Grid& pointState, + const ColorMap& isFirstPiece) +{ + m_startingPoints.init(variant, *m_geo); + auto colors = get_colors(variant); + if (m_isTrigon) + { + bool isFirstPieceAny = false; + for (Color c : colors) + if (isFirstPiece[c]) + { + isFirstPieceAny = true; + break; + } + if (! isFirstPieceAny) + return; + for (Point p : m_startingPoints.get_starting_points(Color(0))) + { + if (! pointState[p].is_empty()) + continue; + int x = m_geo->get_x(p); + int y = m_geo->get_y(p); + qreal fieldX = x * m_fieldWidth; + qreal fieldY = y * m_fieldHeight; + bool isUpward = (m_geo->get_point_type(p) == 0); + Util::paintTriangleStartingPoint(painter, isUpward, fieldX, fieldY, + m_fieldWidth, m_fieldHeight); + } + } + else + { + for (Color c : colors) + { + if (! isFirstPiece[c]) + continue; + for (Point p : m_startingPoints.get_starting_points(c)) + { + if (! pointState[p].is_empty()) + continue; + int x = m_geo->get_x(p); + int y = m_geo->get_y(p); + qreal fieldX = x * m_fieldWidth; + qreal fieldY = y * m_fieldHeight; + if (m_isNexos) + Util::paintSegmentStartingPoint(painter, variant, c, + fieldX, fieldY, + m_fieldWidth); + else + Util::paintSquareStartingPoint(painter, variant, c, fieldX, + fieldY, m_fieldWidth); + } + } + } +} + +//----------------------------------------------------------------------------- diff --git a/src/libpentobi_gui/BoardPainter.h b/src/libpentobi_gui/BoardPainter.h new file mode 100644 index 0000000..560fa67 --- /dev/null +++ b/src/libpentobi_gui/BoardPainter.h @@ -0,0 +1,147 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/BoardPainter.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_GUI_BOARD_PAINTER_H +#define LIBPENTOBI_GUI_BOARD_PAINTER_H + +#include +#include "libpentobi_base/Grid.h" +#include "libpentobi_base/Board.h" + +using libboardgame_base::CoordPoint; +using libboardgame_base::Transform; +using libpentobi_base::Board; +using libpentobi_base::Color; +using libpentobi_base::ColorMap; +using libpentobi_base::Variant; +using libpentobi_base::Geometry; +using libpentobi_base::Grid; +using libpentobi_base::MovePoints; +using libpentobi_base::PieceInfo; +using libpentobi_base::Point; +using libpentobi_base::PointState; +using libpentobi_base::StartingPoints; + +//----------------------------------------------------------------------------- + +/** Paints a board. + The painter can be used without having to create an instance of class Board, + which is undesirable for use cases like the thumbnailer because of the slow + creation of the BoardConst class. Instead, the board state is passed to the + paint() function as a grid of point states. */ +class BoardPainter +{ +public: + enum + { + dot = 1 << 1, + + circle = 1 << 2 + }; + + BoardPainter(); + + ~BoardPainter(); + + void setCoordinates(bool enable) { m_coordinates = enable; } + + void setCoordinateColor(const QColor& color) { m_coordinateColor = color; } + + /** Paint the board. + This function must be called before painting any pieces because it + initializes some members that are used by the piece painting + functions. */ + void paintEmptyBoard(QPainter& painter, unsigned width, unsigned height, + Variant variant, const Geometry& geo); + + /** Paint the pieces and markup. + The pieceId parameter only needs to be initialized in game variant + Nexos and is needed to paint the junctions between segment. Only + segment points of pieceId are used (point type 1 or 2) and must be 0 if + the point is empty or contain a unique value for segments of the same + piece. */ + void paintPieces(QPainter& painter, const Grid& pointState, + const Grid& pieceId, + const Grid* labels = nullptr, + const Grid* marks = nullptr); + + /** Paint the selected piece. + Paints the selected piece either transparent (if not legal) or opaque + (if legal). */ + void paintSelectedPiece(QPainter& painter, Color c, + const MovePoints& points, bool isLegal); + + /** Get the corresponding board coordinates of a pixel. + @return The board coordinates or CoordPoint::null() if paint() was + not called yet or the pixel is outside the board. */ + CoordPoint getCoordPoint(int x, int y); + + bool hasPainted() const { return m_hasPainted; } + +private: + bool m_hasPainted = false; + + bool m_coordinates = false; + + bool m_isTrigon; + + bool m_isNexos; + + bool m_isCallisto; + + const Geometry* m_geo; + + Variant m_variant; + + /** The width of the last board painted. */ + int m_width; + + /** The height of the last board painted. */ + int m_height; + + QColor m_coordinateColor = Qt::black; + + qreal m_fieldWidth; + + qreal m_fieldHeight; + + QPointF m_boardOffset; + + QFont m_font; + + QFont m_fontCondensed; + + QFont m_fontSemiCondensed; + + QFont m_fontCoordLabels; + + StartingPoints m_startingPoints; + + + void paintCoordinates(QPainter& painter); + + void paintJunction(QPainter& painter, Variant variant, + const Grid& pointState, + const Grid& pieceId, int x, int y, + qreal fieldX, qreal fieldY); + + void paintLabel(QPainter& painter, qreal x, qreal y, qreal width, + qreal height, const QString& label, bool isCoordLabel); + + void paintLabels(QPainter& painter, const Grid& pointState, + Variant variant, const Grid& labels); + + void paintMarks(QPainter& painter, const Grid& pointState, + Variant variant, const Grid& marks); + + void paintStartingPoints(QPainter& painter, Variant variant, + const Grid& pointState, + const ColorMap& isFirstPiece); +}; + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_GUI_BOARD_PAINTER_H diff --git a/src/libpentobi_gui/CMakeLists.txt b/src/libpentobi_gui/CMakeLists.txt new file mode 100644 index 0000000..9283f38 --- /dev/null +++ b/src/libpentobi_gui/CMakeLists.txt @@ -0,0 +1,87 @@ +set(CMAKE_AUTOMOC TRUE) + +set(pentobi_gui_STAT_SRCS + BoardPainter.h + BoardPainter.cpp + ComputerColorDialog.h + ComputerColorDialog.cpp + GameInfoDialog.h + GameInfoDialog.cpp + GuiBoard.h + GuiBoard.cpp + GuiBoardUtil.h + GuiBoardUtil.cpp + HelpWindow.h + HelpWindow.cpp + InitialRatingDialog.h + InitialRatingDialog.cpp + LeaveFullscreenButton.h + LeaveFullscreenButton.cpp + LineEdit.h + LineEdit.cpp + OrientationDisplay.h + OrientationDisplay.cpp + PieceSelector.h + PieceSelector.cpp + SameHeightLayout.h + SameHeightLayout.cpp + ScoreDisplay.h + ScoreDisplay.cpp + Util.h + Util.cpp +) + +set(pentobi_gui_ICNS + go-home.png + go-next.png + go-previous.png +) + +set(pentobi_gui_TS + translations/libpentobi_gui_de.ts + ) + +# Create PNG icons from SVG icons using the helper program src/convert +file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/icons) +file(COPY libpentobi_gui_resources.qrc DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) +foreach(icon ${pentobi_gui_ICNS}) + string(REPLACE ".png" ".svg" svgicon ${icon}) + add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/icons/${icon}" + COMMAND convert ${CMAKE_CURRENT_SOURCE_DIR}/icons/${svgicon} + ${CMAKE_CURRENT_BINARY_DIR}/icons/${icon} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/icons/${svgicon} + ) +endforeach() +qt5_add_resources(pentobi_gui_RC_SRCS + ${CMAKE_CURRENT_BINARY_DIR}/libpentobi_gui_resources.qrc + OPTIONS -no-compress) +file(COPY libpentobi_gui_resources_2x.qrc DESTINATION + ${CMAKE_CURRENT_BINARY_DIR}) +foreach(icon ${pentobi_gui_ICNS}) +string(REPLACE ".png" ".svg" svgicon ${icon}) +string(REPLACE ".png" "@2x.png" hdpiicon ${icon}) +add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/icons/${hdpiicon}" + COMMAND convert --hdpi ${CMAKE_CURRENT_SOURCE_DIR}/icons/${svgicon} + ${CMAKE_CURRENT_BINARY_DIR}/icons/${hdpiicon} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/icons/${svgicon} +) +endforeach() +qt5_add_resources(pentobi_gui_RC_SRCS + ${CMAKE_CURRENT_BINARY_DIR}/libpentobi_gui_resources_2x.qrc + OPTIONS -no-compress) + +qt5_add_translation(pentobi_gui_QM_SRCS ${pentobi_gui_TS}) + +add_library(pentobi_gui STATIC + ${pentobi_gui_STAT_SRCS} + ${pentobi_gui_RC_SRCS} + ${pentobi_gui_QM_SRCS}) + +target_link_libraries(pentobi_gui Qt5::Widgets) + +# Install translation files. If you change the destination, you need to +# update the default for PENTOBI_TRANSLATIONS in the main CMakeLists.txt +install(FILES ${pentobi_gui_QM_SRCS} + DESTINATION ${CMAKE_INSTALL_DATADIR}/pentobi/translations) diff --git a/src/libpentobi_gui/ComputerColorDialog.cpp b/src/libpentobi_gui/ComputerColorDialog.cpp new file mode 100644 index 0000000..ec354fb --- /dev/null +++ b/src/libpentobi_gui/ComputerColorDialog.cpp @@ -0,0 +1,85 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/ComputerColorDialog.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "ComputerColorDialog.h" + +#include +#include +#include + +//----------------------------------------------------------------------------- + +ComputerColorDialog::ComputerColorDialog(QWidget* parent, + Variant variant, + ColorMap& computerColor) + : QDialog(parent), + m_computerColor(computerColor), + m_variant(variant) +{ + setWindowTitle(tr("Computer Colors")); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + auto layout = new QVBoxLayout; + setLayout(layout); + layout->setSizeConstraint(QLayout::SetFixedSize); + layout->addWidget(new QLabel(tr("Computer plays:"))); + for (Color::IntType i = 0; i < get_nu_players(m_variant); ++i) + createCheckBox(layout, Color(i)); + auto buttonBox = + new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + layout->addWidget(buttonBox); + connect(buttonBox, SIGNAL(accepted()), SLOT(accept())); + connect(buttonBox, SIGNAL(rejected()), SLOT(reject())); + buttonBox->setFocus(); +} + +void ComputerColorDialog::accept() +{ + auto nuPlayers = get_nu_players(m_variant); + auto nuColors = get_nu_colors(m_variant); + if (nuPlayers == nuColors || m_variant == Variant::classic_3) + for (Color c : Color::Range(nuPlayers)) + m_computerColor[c] = m_checkBox[c.to_int()]->isChecked(); + else + { + LIBBOARDGAME_ASSERT(nuPlayers == 2 && nuColors == 4); + m_computerColor[Color(0)] = m_checkBox[0]->isChecked(); + m_computerColor[Color(2)] = m_checkBox[0]->isChecked(); + m_computerColor[Color(1)] = m_checkBox[1]->isChecked(); + m_computerColor[Color(3)] = m_checkBox[1]->isChecked(); + } + QDialog::accept(); +} + +void ComputerColorDialog::createCheckBox(QLayout* layout, Color c) +{ + auto checkBox = new QCheckBox(getPlayerString(c)); + checkBox->setChecked(m_computerColor[c]); + layout->addWidget(checkBox); + m_checkBox[c.to_int()] = checkBox; +} + +QString ComputerColorDialog::getPlayerString(Color c) +{ + auto nuPlayers = get_nu_players(m_variant); + auto nuColors = get_nu_colors(m_variant); + auto i = c.to_int(); + if (nuPlayers == 2 && nuColors == 4) + return i == 0 || i == 2 ? tr("&Blue/Red") : tr("&Yellow/Green"); + if (i == 0) + return tr("&Blue"); + if (i == 1) + return nuColors == 2 ? tr("&Green") : tr("&Yellow"); + if (i == 2) + return tr("&Red"); + LIBBOARDGAME_ASSERT(i == 3); + return tr("&Green"); +} + +//----------------------------------------------------------------------------- diff --git a/src/libpentobi_gui/ComputerColorDialog.h b/src/libpentobi_gui/ComputerColorDialog.h new file mode 100644 index 0000000..328a366 --- /dev/null +++ b/src/libpentobi_gui/ComputerColorDialog.h @@ -0,0 +1,54 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/ComputerColorDialog.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_GUI_COMPUTER_COLOR_DIALOG_H +#define LIBPENTOBI_GUI_COMPUTER_COLOR_DIALOG_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include "libpentobi_base/Variant.h" +#include "libpentobi_base/ColorMap.h" + +using namespace std; +using libpentobi_base::Variant; +using libpentobi_base::Color; +using libpentobi_base::ColorMap; + +//----------------------------------------------------------------------------- + +class ComputerColorDialog final + : public QDialog +{ + Q_OBJECT + +public: + ComputerColorDialog(QWidget* parent, Variant variant, + ColorMap& computerColor); + +public slots: + void accept() override; + +private: + ColorMap& m_computerColor; + + Variant m_variant; + + array m_checkBox; + + void createCheckBox(QLayout* layout, Color c); + + QString getPlayerString(Color c); +}; + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_GUI_COMPUTER_COLOR_DIALOG_H diff --git a/src/libpentobi_gui/GameInfoDialog.cpp b/src/libpentobi_gui/GameInfoDialog.cpp new file mode 100644 index 0000000..06bad4b --- /dev/null +++ b/src/libpentobi_gui/GameInfoDialog.cpp @@ -0,0 +1,149 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/GameInfoDialog.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "GameInfoDialog.h" + +#include +#include "LineEdit.h" +#include "libpentobi_gui/Util.h" + +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +GameInfoDialog::GameInfoDialog(QWidget* parent, Game& game) + : QDialog(parent), + m_game(game), + m_charset(game.get_root().get_property("CA", "")) +{ + setWindowTitle(tr("Game Info")); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + auto layout = new QVBoxLayout; + setLayout(layout); + m_formLayout = new QFormLayout; + layout->addLayout(m_formLayout); + auto variant = game.get_variant(); + auto nuColors = get_nu_colors(variant); + auto nuPlayers = get_nu_players(variant); + if (nuColors == 2) + { + m_playerBlue = createPlayerName(tr("Player &Blue:"), Color(0)); + m_playerGreen = createPlayerName(tr("Player &Green:"), Color(1)); + } + else if (nuPlayers == 2) + { + m_playerBlueRed = createPlayerName(tr("Player &Blue/Red:"), Color(0)); + m_playerYellowGreen = + createPlayerName(tr("Player &Yellow/Green:"), Color(1)); + } + else + { + m_playerBlue = createPlayerName(tr("Player &Blue:"), Color(0)); + m_playerYellow = createPlayerName(tr("Player &Yellow:"), Color(1)); + m_playerRed = createPlayerName(tr("Player &Red:"), Color(2)); + if (nuPlayers == 4) + m_playerGreen = createPlayerName(tr("Player &Green:"), Color(3)); + } + m_date = createLine(tr("&Date:"), m_game.get_date()); + m_time = createLine(tr("&Time limits:"), m_game.get_time()); + m_event = createLine(tr("&Event:"), m_game.get_event()); + m_round = createLine(tr("R&ound:"), m_game.get_round()); + auto buttonBox = + new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + layout->addWidget(buttonBox); + // We assume that the user wants to edit the game info if it is still empty + // and that he only wants to display it if not empty. Therefore, we leave + // the focus at the first text field if it is empty and put it on the + // button box otherwise. + if (nuColors == 4 && nuPlayers == 2) + { + if (! m_playerBlueRed->text().isEmpty()) + buttonBox->setFocus(); + } + else if (! m_playerBlue->text().isEmpty()) + buttonBox->setFocus(); + connect(buttonBox, SIGNAL(accepted()), SLOT(accept())); + connect(buttonBox, SIGNAL(rejected()), SLOT(reject())); +} + +GameInfoDialog::~GameInfoDialog() +{ +} + +void GameInfoDialog::accept() +{ + auto variant = m_game.get_variant(); + auto nuColors = get_nu_colors(variant); + auto nuPlayers = get_nu_players(variant); + string value; + if (nuColors == 2) + { + if (acceptLine(m_playerBlue, value)) + m_game.set_player_name(Color(0), value); + if (acceptLine(m_playerGreen, value)) + m_game.set_player_name(Color(1), value); + } + else if (nuPlayers == 2) + { + if (acceptLine(m_playerBlueRed, value)) + m_game.set_player_name(Color(0), value); + if (acceptLine(m_playerYellowGreen, value)) + m_game.set_player_name(Color(1), value); + } + else + { + if (acceptLine(m_playerBlue, value)) + m_game.set_player_name(Color(0), value); + if (acceptLine(m_playerYellow, value)) + m_game.set_player_name(Color(1), value); + if (acceptLine(m_playerRed, value)) + m_game.set_player_name(Color(2), value); + if (nuPlayers == 4) + if (acceptLine(m_playerGreen, value)) + m_game.set_player_name(Color(3), value); + } + if (acceptLine(m_date, value)) + m_game.set_date(value); + if (acceptLine(m_time, value)) + m_game.set_time(value); + if (acceptLine(m_event, value)) + m_game.set_event(value); + if (acceptLine(m_round, value)) + m_game.set_round(value); + QDialog::accept(); +} + +bool GameInfoDialog::acceptLine(QLineEdit* lineEdit, string& value) +{ + if (! lineEdit->isModified()) + return false; + QString text = lineEdit->text(); + value = Util::convertSgfValueFromQString(text, m_charset); + return true; +} + +QLineEdit* GameInfoDialog::createLine(const QString& label, const string& text) +{ + auto lineEdit = new LineEdit(30); + if (! text.empty()) + { + lineEdit->setText(Util::convertSgfValueToQString(text, m_charset)); + lineEdit->setCursorPosition(0); + } + m_formLayout->addRow(label, lineEdit); + return lineEdit; +} + +QLineEdit* GameInfoDialog::createPlayerName(const QString& label, Color c) +{ + return createLine(label, m_game.get_player_name(c)); +} + +//----------------------------------------------------------------------------- diff --git a/src/libpentobi_gui/GameInfoDialog.h b/src/libpentobi_gui/GameInfoDialog.h new file mode 100644 index 0000000..f5a9c65 --- /dev/null +++ b/src/libpentobi_gui/GameInfoDialog.h @@ -0,0 +1,75 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/GameInfoDialog.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_GUI_GAME_INFO_DIALOG_H +#define LIBPENTOBI_GUI_GAME_INFO_DIALOG_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include "libpentobi_base/Game.h" + +using namespace std; +using libpentobi_base::Color; +using libpentobi_base::Game; + +//----------------------------------------------------------------------------- + +class GameInfoDialog final + : public QDialog +{ + Q_OBJECT + +public: + GameInfoDialog(QWidget* parent, Game& game); + + ~GameInfoDialog(); + +public slots: + void accept() override; + +private: + Game& m_game; + + string m_charset; + + QFormLayout* m_formLayout; + + QLineEdit* m_playerBlue; + + QLineEdit* m_playerYellow; + + QLineEdit* m_playerRed; + + QLineEdit* m_playerGreen; + + QLineEdit* m_playerBlueRed; + + QLineEdit* m_playerYellowGreen; + + QLineEdit* m_date; + + QLineEdit* m_event; + + QLineEdit* m_round; + + QLineEdit* m_time; + + bool acceptLine(QLineEdit* lineEdit, string& value); + + QLineEdit* createLine(const QString& label, const string& text); + + QLineEdit* createPlayerName(const QString& label, Color c); +}; + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_GUI_GAME_INFO_DIALOG_H diff --git a/src/libpentobi_gui/GuiBoard.cpp b/src/libpentobi_gui/GuiBoard.cpp new file mode 100644 index 0000000..6c0e525 --- /dev/null +++ b/src/libpentobi_gui/GuiBoard.cpp @@ -0,0 +1,520 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/GuiBoard.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "GuiBoard.h" + +#include +#include +#include "libboardgame_base/Transform.h" + +using namespace std; +using libboardgame_base::Transform; +using libpentobi_base::Geometry; +using libpentobi_base::MovePoints; +using libpentobi_base::PiecePoints; +using libpentobi_base::PieceSet; +using libpentobi_base::Point; +using libpentobi_base::PointState; + +//----------------------------------------------------------------------------- + +namespace { + +bool allPointEmpty(const Board& bd, Move mv) +{ + for (Point p : bd.get_move_points(mv)) + if (! bd.get_point_state(p).is_empty()) + return false; + return true; +} + +QPixmap* createPixmap(const QPainter& painter, const QSize& size) +{ + auto devicePixelRatio = painter.device()->devicePixelRatio(); + auto pixmap = new QPixmap(devicePixelRatio * size); + pixmap->setDevicePixelRatio(devicePixelRatio); + return pixmap; +} + +} // namespace + +//----------------------------------------------------------------------------- + +GuiBoard::GuiBoard(QWidget* parent, const Board& bd) + : QWidget(parent), + m_bd(bd) +{ + setMinimumSize(350, 350); + connect(&m_currentMoveShownAnimationTimer, SIGNAL(timeout()), + SLOT(showMoveAnimation())); +} + +void GuiBoard::changeEvent(QEvent* event) +{ + if (event->type() == QEvent::StyleChange) + setEmptyBoardDirty(); +} + +void GuiBoard::clearMarkup() +{ + for (Point p : m_bd) + { + m_marks[p] = 0; + setLabel(p, ""); + } +} + +void GuiBoard::clearPiece() +{ + m_selectedPiece = Piece::null(); + m_selectedPieceTransform = nullptr; + setSelectedPiecePoints(); + setMouseTracking(false); +} + +void GuiBoard::copyFromBoard(const Board& bd) +{ + auto& geo = bd.get_geometry(); + auto variant = bd.get_variant(); + m_pointState.copy_from(bd.get_point_state(), geo); + auto pieceSet = get_piece_set(variant); + if (pieceSet == PieceSet::nexos || pieceSet == PieceSet::callisto) + { + m_pieceId.fill(0, geo); + unsigned n = 0; + for (Color c : bd.get_colors()) + for (Move mv : bd.get_setup().placements[c]) + { + ++n; + for (Point p : bd.get_move_points(mv)) + m_pieceId[p] = n; + } + for (auto mv : bd.get_moves()) + { + ++n; + for (Point p : bd.get_move_points(mv.move)) + m_pieceId[p] = n; + } + } + if (! m_isInitialized || m_variant != variant) + { + m_variant = variant; + m_isInitialized = true; + m_labels.fill("", geo); + m_marks.fill(0, geo); + setEmptyBoardDirty(); + } + else + setDirty(); +} + +Move GuiBoard::findSelectedPieceMove() +{ + if (m_selectedPiece.is_null() || m_selectedPieceOffset.is_null()) + return Move::null(); + const PiecePoints& points = + m_bd.get_piece_info(m_selectedPiece).get_points(); + auto& geo = m_bd.get_geometry(); + int width = static_cast(geo.get_width()); + int height = static_cast(geo.get_height()); + MovePoints movePoints; + for (CoordPoint p : points) + { + p = m_selectedPieceTransform->get_transformed(p); + int x = p.x + m_selectedPieceOffset.x; + int y = p.y + m_selectedPieceOffset.y; + if (x < 0 || x >= width || y < 0 || y >= height) + return Move::null(); + Point pp = geo.get_point(x, y); + if (pp.is_null()) + return Move::null(); + movePoints.push_back(pp); + } + Move mv; + if (! m_bd.find_move(movePoints, m_selectedPiece, mv) + || (m_freePlacement && ! allPointEmpty(m_bd, mv)) + || (! m_freePlacement + && ! m_bd.is_legal(m_selectedPieceColor, mv))) + return Move::null(); + else + return mv; +} + +void GuiBoard::leaveEvent(QEvent*) +{ + m_selectedPieceOffset = CoordPoint::null(); + setSelectedPiecePoints(); +} + +void GuiBoard::mouseMoveEvent(QMouseEvent* event) +{ + if (m_selectedPiece.is_null()) + return; + CoordPoint oldOffset = m_selectedPieceOffset; + setSelectedPieceOffset(*event); + if (m_selectedPieceOffset != oldOffset) + setSelectedPiecePoints(); +} + +void GuiBoard::mousePressEvent(QMouseEvent* event) +{ + if (m_selectedPiece.is_null()) + { + CoordPoint p = m_boardPainter.getCoordPoint(event->x(), event->y()); + auto& geo = m_bd.get_geometry(); + if (geo.is_onboard(p)) + emit pointClicked(geo.get_point(p.x, p.y)); + return; + } + setSelectedPieceOffset(*event); + placePiece(); +} + +void GuiBoard::movePieceDown() +{ + if (m_selectedPiece.is_null()) + return; + auto& geo = m_bd.get_geometry(); + CoordPoint newOffset; + if (m_selectedPieceOffset.is_null()) + { + newOffset = CoordPoint(geo.get_width() / 2, 0); + setSelectedPieceOffset(newOffset); + setSelectedPiecePoints(); + } + else + { + newOffset = m_selectedPieceOffset; + if (m_bd.get_piece_set() == PieceSet::trigon) + { + if (m_selectedPieceOffset.x % 2 == 0) + ++newOffset.x; + else + --newOffset.x; + ++newOffset.y; + } + else + newOffset.y += geo.get_period_y(); + if (geo.is_onboard(newOffset)) + { + setSelectedPieceOffset(newOffset); + setSelectedPiecePoints(); + } + } +} + +void GuiBoard::movePieceLeft() +{ + if (m_selectedPiece.is_null()) + return; + auto& geo = m_bd.get_geometry(); + CoordPoint newOffset; + if (m_selectedPieceOffset.is_null()) + { + newOffset = CoordPoint(geo.get_width() - 1, geo.get_height() / 2); + setSelectedPieceOffset(newOffset); + setSelectedPiecePoints(); + } + else + { + newOffset = m_selectedPieceOffset; + newOffset.x -= geo.get_period_x(); + if (geo.is_onboard(newOffset)) + { + setSelectedPieceOffset(newOffset); + setSelectedPiecePoints(); + } + } +} + +void GuiBoard::movePieceRight() +{ + if (m_selectedPiece.is_null()) + return; + auto& geo = m_bd.get_geometry(); + CoordPoint newOffset; + if (m_selectedPieceOffset.is_null()) + { + newOffset = CoordPoint(0, geo.get_height() / 2); + setSelectedPieceOffset(newOffset); + setSelectedPiecePoints(); + } + else + { + newOffset = m_selectedPieceOffset; + newOffset.x += geo.get_period_x(); + if (geo.is_onboard(newOffset)) + { + setSelectedPieceOffset(newOffset); + setSelectedPiecePoints(); + } + } +} + +void GuiBoard::movePieceUp() +{ + if (m_selectedPiece.is_null()) + return; + auto& geo = m_bd.get_geometry(); + CoordPoint newOffset; + if (m_selectedPieceOffset.is_null()) + { + newOffset = CoordPoint(geo.get_width() / 2, geo.get_height() - 1); + setSelectedPieceOffset(newOffset); + setSelectedPiecePoints(); + } + else + { + newOffset = m_selectedPieceOffset; + if (m_bd.get_piece_set() == PieceSet::trigon) + { + if (m_selectedPieceOffset.x % 2 == 0) + ++newOffset.x; + else + --newOffset.x; + --newOffset.y; + } + else + newOffset.y -= geo.get_period_y(); + if (geo.is_onboard(newOffset)) + { + setSelectedPieceOffset(newOffset); + setSelectedPiecePoints(); + } + } +} + +void GuiBoard::paintEvent(QPaintEvent*) +{ + if (! m_isInitialized) + return; + QPainter painter(this); + if (! m_emptyBoardPixmap || m_emptyBoardPixmap->size() != size()) + { + m_emptyBoardPixmap.reset(createPixmap(painter, size())); + m_emptyBoardDirty = true; + } + if (! m_boardPixmap || m_boardPixmap->size() != size()) + { + m_boardPixmap.reset(createPixmap(painter, size())); + m_dirty = true; + } + if (m_emptyBoardDirty) + { + QColor coordLabelColor = + QApplication::palette().color(QPalette::WindowText); + m_boardPainter.setCoordinateColor(coordLabelColor); + m_emptyBoardPixmap->fill(Qt::transparent); + QPainter painter(m_emptyBoardPixmap.get()); + m_boardPainter.paintEmptyBoard(painter, width(), height(), m_variant, + m_bd.get_geometry()); + m_emptyBoardDirty = false; + } + if (m_dirty) + { + m_boardPixmap->fill(Qt::transparent); + QPainter painter(m_boardPixmap.get()); + painter.drawPixmap(0, 0, *m_emptyBoardPixmap); + m_boardPainter.paintPieces(painter, m_pointState, m_pieceId, &m_labels, + &m_marks); + m_dirty = false; + } + painter.drawPixmap(0, 0, *m_boardPixmap); + if (m_isMoveShown) + { + if (m_currentMoveShownAnimationIndex % 2 == 0) + m_boardPainter.paintSelectedPiece(painter, m_currentMoveShownColor, + m_currentMoveShownPoints, true); + } + else if (! m_selectedPiecePoints.empty()) + { + bool isLegal = ! findSelectedPieceMove().is_null(); + m_boardPainter.paintSelectedPiece(painter, m_selectedPieceColor, + m_selectedPiecePoints, isLegal); + } +} + +void GuiBoard::placePiece() +{ + auto mv = findSelectedPieceMove(); + if (! mv.is_null()) + emit play(m_selectedPieceColor, mv); +} + +void GuiBoard::selectPiece(Color color, Piece piece) +{ + if (m_selectedPiece == piece && m_selectedPieceColor == color) + return; + m_selectedPieceColor = color; + m_selectedPieceTransform = m_bd.get_transforms().get_default(); + if (m_selectedPiece.is_null()) + m_selectedPieceOffset = CoordPoint::null(); + m_selectedPiece = piece; + setSelectedPieceOffset(m_selectedPieceOffset); + setSelectedPiecePoints(); + setMouseTracking(true); +} + +void GuiBoard::setEmptyBoardDirty() +{ + m_emptyBoardDirty = true; + m_dirty = true; + update(); +} + +void GuiBoard::setDirty() +{ + m_dirty = true; + update(); +} + +void GuiBoard::setCoordinates(bool enable) +{ + m_boardPainter.setCoordinates(enable); + setEmptyBoardDirty(); +} + +void GuiBoard::setFreePlacement(bool enable) +{ + m_freePlacement = enable; + update(); +} + +void GuiBoard::setLabel(Point p, const QString& text) +{ + if (! m_isInitialized) + return; + if (m_labels[p] != text) + { + m_labels[p] = text; + setDirty(); + } +} + +void GuiBoard::setMark(Point p, int mark, bool enable) +{ + if (! m_isInitialized) + return; + if (((m_marks[p] & mark) != 0) != enable) + { + m_marks[p] ^= mark; + setDirty(); + } +} + +void GuiBoard::setSelectedPieceOffset(const QMouseEvent& event) +{ + setSelectedPieceOffset(m_boardPainter.getCoordPoint(event.x(), event.y())); +} + +void GuiBoard::setSelectedPieceOffset(const CoordPoint& offset) +{ + if (offset.is_null()) + { + m_selectedPieceOffset = offset; + return; + } + auto& geo = m_bd.get_geometry(); + auto pieceSet = m_bd.get_piece_set(); + unsigned old_point_type = geo.get_point_type(offset); + CoordPoint type_matched_offset = offset; + if (pieceSet == PieceSet::trigon) + { + // Offset must match the point type (triangle up/down) of + // CoordPoint(0, 0) after the piece transformation + unsigned point_type = m_selectedPieceTransform->get_new_point_type(); + bool hasLeft = geo.is_onboard(CoordPoint(offset.x - 1, offset.y)); + bool hasRight = geo.is_onboard(CoordPoint(offset.x + 1, offset.y)); + if (old_point_type != point_type) + { + if ((point_type == 0 && hasRight) + || (point_type == 1 && ! hasLeft)) + ++type_matched_offset.x; + else + --type_matched_offset.x; + } + } + if (pieceSet == PieceSet::nexos) + { + // Offset must be a junction + if (old_point_type == 1) // horiz. segment + --type_matched_offset.x; + else if (old_point_type == 2) // vert. segment + --type_matched_offset.y; + else if (old_point_type == 3) // hole + { + --type_matched_offset.x; + --type_matched_offset.y; + } + } + m_selectedPieceOffset = type_matched_offset; +} + +void GuiBoard::setSelectedPiecePoints(Move mv) +{ + m_selectedPiecePoints.clear(); + for (Point p : m_bd.get_move_points(mv)) + m_selectedPiecePoints.push_back(p); + update(); +} + +void GuiBoard::setSelectedPiecePoints() +{ + m_selectedPiecePoints.clear(); + if (! m_selectedPiece.is_null() && ! m_selectedPieceOffset.is_null()) + { + auto& geo = m_bd.get_geometry(); + int width = static_cast(geo.get_width()); + int height = static_cast(geo.get_height()); + for (CoordPoint p : m_bd.get_piece_info(m_selectedPiece).get_points()) + { + p = m_selectedPieceTransform->get_transformed(p); + int x = p.x + m_selectedPieceOffset.x; + int y = p.y + m_selectedPieceOffset.y; + if (x >= 0 && x < width && y >= 0 && y < height) + m_selectedPiecePoints.push_back(geo.get_point(x, y)); + } + } + update(); +} + +void GuiBoard::setSelectedPieceTransform(const Transform* transform) +{ + if (m_selectedPieceTransform == transform) + return; + m_selectedPieceTransform = transform; + setSelectedPieceOffset(m_selectedPieceOffset); + setSelectedPiecePoints(); +} + +void GuiBoard::showMove(Color c, Move mv) +{ + m_isMoveShown = true; + m_currentMoveShownColor = c; + m_currentMoveShownPoints.clear(); + for (Point p : m_bd.get_move_points(mv)) + m_currentMoveShownPoints.push_back(p); + m_currentMoveShownAnimationIndex = 0; + m_currentMoveShownAnimationTimer.start(500); + update(); +} + +void GuiBoard::showMoveAnimation() +{ + ++m_currentMoveShownAnimationIndex; + if (m_currentMoveShownAnimationIndex > 5) + { + m_isMoveShown = false; + m_currentMoveShownAnimationTimer.stop(); + } + update(); +} + +//----------------------------------------------------------------------------- diff --git a/src/libpentobi_gui/GuiBoard.h b/src/libpentobi_gui/GuiBoard.h new file mode 100644 index 0000000..dc48ef3 --- /dev/null +++ b/src/libpentobi_gui/GuiBoard.h @@ -0,0 +1,188 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/GuiBoard.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_GUI_GUI_BOARD_H +#define LIBPENTOBI_GUI_GUI_BOARD_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include "BoardPainter.h" +#include "libboardgame_base/CoordPoint.h" +#include "libpentobi_base/Board.h" + +using namespace std; +using libpentobi_base::Color; +using libboardgame_base::CoordPoint; +using libpentobi_base::Board; +using libpentobi_base::Grid; +using libpentobi_base::Move; +using libpentobi_base::Piece; +using libpentobi_base::PieceInfo; +using libpentobi_base::Point; + +//----------------------------------------------------------------------------- + +class GuiBoard + : public QWidget +{ + Q_OBJECT + +public: + GuiBoard(QWidget* parent, const Board& bd); + + void setCoordinates(bool enable); + + const Board& getBoard() const; + + const Grid& getLabels() const; + + Piece getSelectedPiece() const; + + const Transform* getSelectedPieceTransform() const; + + void setSelectedPieceTransform(const Transform* transform); + + void showMove(Color c, Move mv); + + void copyFromBoard(const Board& bd); + + void setLabel(Point p, const QString& text); + + void setMark(Point p, int mark, bool enable = true); + + void clearMarkup(); + + void setFreePlacement(bool enable); + + void setSelectedPiecePoints(Move mv); + +public slots: + void clearPiece(); + + void selectPiece(Color color, Piece piece); + + void movePieceLeft(); + + void movePieceRight(); + + void movePieceUp(); + + void movePieceDown(); + + void placePiece(); + +signals: + void play(Color color, Move mv); + + void pointClicked(Point p); + +protected: + void changeEvent(QEvent* event) override; + + void leaveEvent(QEvent* event) override; + + void mouseMoveEvent(QMouseEvent* event) override; + + void mousePressEvent(QMouseEvent* event) override; + + void paintEvent(QPaintEvent* event) override; + +private: + const Board& m_bd; + + bool m_isInitialized = false; + + bool m_freePlacement = false; + + /** Does the empty board need redrawing? */ + bool m_emptyBoardDirty = true; + + /** Do the pieces and markup on the board need redrawing? + If true, the cached board pixmap needs to be repainted. This does not + include the selected piece (the selected piece is always painted). */ + bool m_dirty = true; + + bool m_isMoveShown = false; + + Variant m_variant; + + Board::PointStateGrid m_pointState; + + Grid m_pieceId; + + Piece m_selectedPiece = Piece::null(); + + Color m_selectedPieceColor; + + const Transform* m_selectedPieceTransform = nullptr; + + CoordPoint m_selectedPieceOffset; + + MovePoints m_selectedPiecePoints; + + Grid m_labels; + + Grid m_marks; + + BoardPainter m_boardPainter; + + unique_ptr m_emptyBoardPixmap; + + unique_ptr m_boardPixmap; + + Color m_currentMoveShownColor; + + MovePoints m_currentMoveShownPoints; + + int m_currentMoveShownAnimationIndex; + + QTimer m_currentMoveShownAnimationTimer; + + Move findSelectedPieceMove(); + + void setEmptyBoardDirty(); + + void setDirty(); + + void setSelectedPieceOffset(const QMouseEvent& event); + + void setSelectedPieceOffset(const CoordPoint& offset); + + void setSelectedPiecePoints(); + +private slots: + void showMoveAnimation(); +}; + +inline const Board& GuiBoard::getBoard() const +{ + return m_bd; +} + +inline const Grid& GuiBoard::getLabels() const +{ + return m_labels; +} + +inline Piece GuiBoard::getSelectedPiece() const +{ + return m_selectedPiece; +} + +inline const Transform* GuiBoard::getSelectedPieceTransform() const +{ + return m_selectedPieceTransform; +} + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_GUI_GUI_BOARD_H diff --git a/src/libpentobi_gui/GuiBoardUtil.cpp b/src/libpentobi_gui/GuiBoardUtil.cpp new file mode 100644 index 0000000..d560eeb --- /dev/null +++ b/src/libpentobi_gui/GuiBoardUtil.cpp @@ -0,0 +1,115 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/GuiBoardUtil.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "GuiBoardUtil.h" + +#include "libboardgame_sgf/SgfUtil.h" +#include "libboardgame_util/StringUtil.h" + +namespace gui_board_util { + +using libpentobi_base::ColorMove; +using libpentobi_base::PentobiTree; +using libboardgame_sgf::SgfNode; +using libboardgame_sgf::util::is_main_variation; +using libboardgame_sgf::util::get_move_annotation; +using libboardgame_util::get_letter_coord; + +//----------------------------------------------------------------------------- + +namespace { + +/** Get the index of a variation. + This ignores child nodes without moves so that the moves are still labeled + 1a, 1b, 1c, etc. even if this does not correspond to the child node + index. (Note that this is a different convention from variation strings + which does not use move number and child move index, but node depth and + child node index) */ +bool getVariationIndex(const PentobiTree& tree, const SgfNode& node, + unsigned& moveIndex) +{ + auto parent = node.get_parent_or_null(); + if (! parent || parent->has_single_child()) + return false; + unsigned nuSiblingMoves = 0; + moveIndex = 0; + for (auto& i : parent->get_children()) + { + if (! tree.has_move(i)) + continue; + if (&i == &node) + moveIndex = nuSiblingMoves; + ++nuSiblingMoves; + } + if (nuSiblingMoves == 1) + return false; + return true; +} + +void markMove(GuiBoard& guiBoard, const Game& game, const SgfNode& node, + unsigned moveNumber, ColorMove mv, bool markVariations, + bool markWithDot) +{ + if (mv.is_null()) + return; + auto& bd = game.get_board(); + Point p = bd.get_move_info_ext_2(mv.move).label_pos; + if (markWithDot) + { + if (markVariations && ! is_main_variation(game.get_current())) + guiBoard.setMark(p, BoardPainter::circle); + else + guiBoard.setMark(p, BoardPainter::dot); + return; + } + QString label; + label.setNum(moveNumber); + if (markVariations) + { + unsigned moveIndex; + if (getVariationIndex(game.get_tree(), node, moveIndex)) + label.append(get_letter_coord(moveIndex).c_str()); + } + label.append(get_move_annotation(game.get_tree(), node)); + guiBoard.setLabel(p, label); +} + +} // namespace + +//----------------------------------------------------------------------------- + +void setMarkup(GuiBoard& guiBoard, const Game& game, unsigned markMovesBegin, + unsigned markMovesEnd, bool markVariations, bool markWithDot) +{ + guiBoard.clearMarkup(); + if (markMovesBegin == 0) + return; + auto& tree = game.get_tree(); + auto& bd = game.get_board(); + unsigned moveNumber = bd.get_nu_moves(); + auto node = &game.get_current(); + do + { + auto mv = tree.get_move_ignore_invalid(*node); + if (! mv.is_null()) + { + if (moveNumber >= markMovesBegin && moveNumber <= markMovesEnd) + markMove(guiBoard, game, *node, moveNumber, mv, markVariations, + markWithDot); + --moveNumber; + } + node = node->get_parent_or_null(); + } + while (node); +} + +//----------------------------------------------------------------------------- + +} // namespace gui_board_util diff --git a/src/libpentobi_gui/GuiBoardUtil.h b/src/libpentobi_gui/GuiBoardUtil.h new file mode 100644 index 0000000..951427b --- /dev/null +++ b/src/libpentobi_gui/GuiBoardUtil.h @@ -0,0 +1,27 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/GuiBoardUtil.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_GUI_GUI_BOARD_UTIL_H +#define LIBPENTOBI_GUI_GUI_BOARD_UTIL_H + +#include "GuiBoard.h" +#include "libpentobi_base/Game.h" + +namespace gui_board_util { + +using libpentobi_base::Game; + +//----------------------------------------------------------------------------- + +void setMarkup(GuiBoard& guiBoard, const Game& game, + unsigned markMovesBegin, unsigned markMovesEnd, + bool markVariations, bool markWithDot); + +//----------------------------------------------------------------------------- + +} // namespace gui_board_util + +#endif // LIBPENTOBI_GUI_GUI_BOARD_UTIL_H diff --git a/src/libpentobi_gui/HelpWindow.cpp b/src/libpentobi_gui/HelpWindow.cpp new file mode 100644 index 0000000..0848c33 --- /dev/null +++ b/src/libpentobi_gui/HelpWindow.cpp @@ -0,0 +1,118 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/HelpWindow.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "HelpWindow.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include "libboardgame_util/Log.h" + +//----------------------------------------------------------------------------- + +namespace { + +void setIcon(QAction* action, const QString& name) +{ + QString fallback = QString(":/libpentobi_gui/icons/%1.png").arg(name); + action->setIcon(QIcon::fromTheme(name, QIcon(fallback))); +} + +} // namespace + +//----------------------------------------------------------------------------- + +HelpWindow::HelpWindow(QWidget* parent, const QString& title, + const QString& mainPage) + : QMainWindow(parent) +{ + LIBBOARDGAME_LOG("Loading ", mainPage.toLocal8Bit().constData()); + setWindowTitle(title); + if (QIcon::hasThemeIcon("help-browser")) + setWindowIcon(QIcon::fromTheme("help-browser")); + m_mainPageUrl = QUrl::fromLocalFile(mainPage); + auto browser = new QTextBrowser; + setCentralWidget(browser); + browser->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + browser->setSource(m_mainPageUrl); + auto actionBack = new QAction(tr("Back"), this); + actionBack->setToolTip(tr("Show previous page in history")); + actionBack->setEnabled(false); + setIcon(actionBack, "go-previous"); + connect(actionBack, SIGNAL(triggered()), browser, SLOT(backward())); + connect(browser, SIGNAL(backwardAvailable(bool)), + actionBack, SLOT(setEnabled(bool))); + auto actionForward = new QAction(tr("Forward"), this); + actionForward->setToolTip(tr("Show next page in history")); + actionForward->setEnabled(false); + setIcon(actionForward, "go-next"); + connect(actionForward, SIGNAL(triggered()), browser, SLOT(forward())); + connect(browser, SIGNAL(forwardAvailable(bool)), + actionForward, SLOT(setEnabled(bool))); + m_actionHome = new QAction(tr("Contents"), this); + m_actionHome->setToolTip(tr("Show table of contents")); + m_actionHome->setEnabled(false); + setIcon(m_actionHome, "go-home"); + connect(m_actionHome, SIGNAL(triggered()), browser, SLOT(home())); + connect(browser, SIGNAL(sourceChanged(const QUrl&)), + SLOT(handleSourceChanged(const QUrl&))); + auto actionClose = new QAction("", this); + actionClose->setShortcut(QKeySequence::Close); + connect(actionClose, SIGNAL(triggered()), SLOT(hide())); + addAction(actionClose); + auto toolBar = new QToolBar; + toolBar->setMovable(false); + toolBar->setToolButtonStyle(Qt::ToolButtonFollowStyle); + toolBar->addAction(actionBack); + toolBar->addAction(actionForward); + toolBar->addAction(m_actionHome); + addToolBar(toolBar); + QSettings settings; + if (! restoreGeometry(settings.value("helpwindow_geometry").toByteArray())) + adjustSize(); +} + +QString HelpWindow::findMainPage(QString helpDir, QString appName) +{ + auto locale = QLocale::system().name(); + auto path = QString("%1/%2/%3/index.html").arg(helpDir, locale, appName); + if (QFile(path).exists()) + return path; + path = QString("%1/%2/%3/index.html") + .arg(helpDir, locale.split("_")[0], appName); + if (QFile(path).exists()) + return path; + return QString("%1/C/%3/index.html").arg(helpDir, appName); +} + +void HelpWindow::closeEvent(QCloseEvent* event) +{ + QSettings settings; + settings.setValue("helpwindow_geometry", saveGeometry()); + QMainWindow::closeEvent(event); +} + +void HelpWindow::handleSourceChanged(const QUrl& src) +{ + m_actionHome->setEnabled(src != m_mainPageUrl); +} + +QSize HelpWindow::sizeHint() const +{ + auto geo = QApplication::desktop()->screenGeometry(); + return QSize(geo.width() * 4 / 10, geo.height() * 9 / 10); +} + +//----------------------------------------------------------------------------- diff --git a/src/libpentobi_gui/HelpWindow.h b/src/libpentobi_gui/HelpWindow.h new file mode 100644 index 0000000..d26328c --- /dev/null +++ b/src/libpentobi_gui/HelpWindow.h @@ -0,0 +1,52 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/HelpWindow.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_GUI_HELP_WINDOW_H +#define LIBPENTOBI_GUI_HELP_WINDOW_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include + +//----------------------------------------------------------------------------- + +class HelpWindow + : public QMainWindow +{ + Q_OBJECT + +public: + /** Find the main page for a given language. + Assumes that the layout of the help directory is according to + http://www.freedesktop.org/wiki/Specifications/help-spec/ + @param helpDir The help directory. + @param appName The subdirectory name for the application. + @return The full path of index.html. */ + static QString findMainPage(QString helpDir, QString appName); + + HelpWindow(QWidget* parent, const QString& title, const QString& mainPage); + + QSize sizeHint() const override; + +protected: + void closeEvent(QCloseEvent* event) override; + +private: + QUrl m_mainPageUrl; + + QAction* m_actionHome; + +private slots: + void handleSourceChanged(const QUrl& src); +}; + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_GUI_HELP_WINDOW_H diff --git a/src/libpentobi_gui/InitialRatingDialog.cpp b/src/libpentobi_gui/InitialRatingDialog.cpp new file mode 100644 index 0000000..80144a8 --- /dev/null +++ b/src/libpentobi_gui/InitialRatingDialog.cpp @@ -0,0 +1,61 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/InitialRatingDialog.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "InitialRatingDialog.h" + +#include +#include +#include +#include + +//----------------------------------------------------------------------------- + +InitialRatingDialog::InitialRatingDialog(QWidget* parent) + : QDialog(parent) +{ + setWindowTitle(tr("Initial Rating")); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + auto layout = new QVBoxLayout; + setLayout(layout); + layout->setSizeConstraint(QLayout::SetFixedSize); + auto label = + new QLabel(tr("You have not yet played rated games in this game" + " variant. Estimate your playing strength to" + " initialize your rating.")); + label->setWordWrap(true); + layout->addWidget(label); + auto sliderBoxLayout = new QHBoxLayout; + layout->addLayout(sliderBoxLayout); + sliderBoxLayout->addWidget(new QLabel(tr("Beginner"))); + m_slider = new QSlider(Qt::Horizontal); + m_slider->setMinimum(1000); + m_slider->setMaximum(2000); + m_slider->setSingleStep(10); + m_slider->setPageStep(100); + sliderBoxLayout->addWidget(m_slider); + sliderBoxLayout->addWidget(new QLabel(tr("Expert"))); + m_ratingLabel = new QLabel; + layout->addWidget(m_ratingLabel); + setRating(1000); + connect(m_slider, SIGNAL(valueChanged(int)), SLOT(setRating(int))); + auto buttonBox = + new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + layout->addWidget(buttonBox); + connect(buttonBox, SIGNAL(accepted()), SLOT(accept())); + connect(buttonBox, SIGNAL(rejected()), SLOT(reject())); +} + +void InitialRatingDialog::setRating(int rating) +{ + m_rating = rating; + m_ratingLabel->setText(tr("Your initial rating: %1").arg(rating)); +} + +//----------------------------------------------------------------------------- diff --git a/src/libpentobi_gui/InitialRatingDialog.h b/src/libpentobi_gui/InitialRatingDialog.h new file mode 100644 index 0000000..651370c --- /dev/null +++ b/src/libpentobi_gui/InitialRatingDialog.h @@ -0,0 +1,53 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/InitialRatingDialog.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_GUI_INITIAL_RATING_DIALOG_H +#define LIBPENTOBI_GUI_INITIAL_RATING_DIALOG_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +class QLabel; +class QSlider; + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Dialog that asks the user to estimate his initial rating. */ +class InitialRatingDialog final + : public QDialog +{ + Q_OBJECT + +public: + explicit InitialRatingDialog(QWidget* parent); + + int getRating() const; + +public slots: + void setRating(int rating); + +private: + int m_rating; + + QSlider* m_slider; + + QLabel* m_ratingLabel; +}; + +inline int InitialRatingDialog::getRating() const +{ + return m_rating; +} + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_GUI_INITIAL_RATING_DIALOG_H diff --git a/src/libpentobi_gui/LeaveFullscreenButton.cpp b/src/libpentobi_gui/LeaveFullscreenButton.cpp new file mode 100644 index 0000000..2f96636 --- /dev/null +++ b/src/libpentobi_gui/LeaveFullscreenButton.cpp @@ -0,0 +1,79 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/LeaveFullscreenButton.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "LeaveFullscreenButton.h" + +#include +#include +#include +#include +#include + +//----------------------------------------------------------------------------- + +LeaveFullscreenButton::LeaveFullscreenButton(QWidget* parent, QAction* action) + : QObject(parent) +{ + m_timer = new QTimer; + m_timer->setSingleShot(true); + m_triggerArea = new QWidget(parent); + m_triggerArea->setMouseTracking(true); + m_button = new QToolButton(parent); + m_button->setDefaultAction(action); + m_button->setToolTip(""); + m_button->setToolButtonStyle(Qt::ToolButtonTextOnly); + m_button->show(); + // Resize to size hint as a workaround for a bug that clips the + // long button text (tested on Qt 4.8.3 on Linux/KDE). + m_button->resize(m_button->sizeHint()); + int x = qApp->desktop()->screenGeometry().width() - m_button->width(); + m_buttonPos = QPoint(x, 0); + m_triggerArea->resize(m_button->width(), m_button->height() / 2); + m_triggerArea->move(m_buttonPos); + m_animation = new QPropertyAnimation(m_button, "pos"); + m_animation->setDuration(1000); + m_animation->setStartValue(m_buttonPos); + m_animation->setEndValue(QPoint(x, -m_button->height() + 5)); + qApp->installEventFilter(this); + connect(m_timer, SIGNAL(timeout()), SLOT(slideOut())); +} + +void LeaveFullscreenButton::hideButton() +{ + m_animation->stop(); + m_timer->stop(); + m_triggerArea->hide(); + m_button->hide(); +} + +bool LeaveFullscreenButton::eventFilter(QObject* watched, QEvent* event) +{ + if (m_button->isVisible() && event->type() == QEvent::MouseMove + && (watched == m_triggerArea || watched == m_button)) + showButton(); + return false; +} + +void LeaveFullscreenButton::showButton() +{ + m_animation->stop(); + m_button->move(m_buttonPos); + m_button->show(); + m_triggerArea->hide(); + m_timer->start(5000); +} + +void LeaveFullscreenButton::slideOut() +{ + m_triggerArea->show(); + m_animation->start(); +} + +//----------------------------------------------------------------------------- diff --git a/src/libpentobi_gui/LeaveFullscreenButton.h b/src/libpentobi_gui/LeaveFullscreenButton.h new file mode 100644 index 0000000..19d85cd --- /dev/null +++ b/src/libpentobi_gui/LeaveFullscreenButton.h @@ -0,0 +1,68 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/LeaveFullscreenButton.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_GUI_LEAVE_FULLSCREEN_BUTTON_H +#define LIBPENTOBI_GUI_LEAVE_FULLSCREEN_BUTTON_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include + +class QAction; +class QPropertyAnimation; +class QTimer; +class QToolButton; + +//----------------------------------------------------------------------------- + +/** A button at the top right of the screen to leave fullscreen mode that + slides of the screen after a few seconds. + A few pixels of the button stay visible and also an invisible slightly + larger trigger area. If the mouse is moved over this area, the button + becomes visible again. */ +class LeaveFullscreenButton + : public QObject +{ + Q_OBJECT + +public: + /** Constructor. + @param parent The widget that will become fullscreen. This class adds + two child widgets to the parent: the actual button and the trigger area + (an invisible widget that listens for mouse movements and triggers the + button to become visible again if it is slid out). + @param action The action for leaving fullscreen mode associated with + the button */ + LeaveFullscreenButton(QWidget* parent, QAction* action); + + bool eventFilter(QObject* watched, QEvent* event) override; + + void showButton(); + + void hideButton(); + +private: + QToolButton* m_button; + + QWidget* m_triggerArea; + + QPoint m_buttonPos; + + QTimer* m_timer; + + QPropertyAnimation* m_animation; + +private slots: + void slideOut(); +}; + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_GUI_LEAVE_FULLSCREEN_BUTTON_H diff --git a/src/libpentobi_gui/LineEdit.cpp b/src/libpentobi_gui/LineEdit.cpp new file mode 100644 index 0000000..34d6d85 --- /dev/null +++ b/src/libpentobi_gui/LineEdit.cpp @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/LineEdit.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "LineEdit.h" + +#include + +//----------------------------------------------------------------------------- + +LineEdit::LineEdit(int nuCharactersHint) + : m_nuCharactersHint(nuCharactersHint) +{ +} + +QSize LineEdit::sizeHint() const +{ + QFont font = QApplication::font(); + QFontMetrics metrics(font); + QSize size = QLineEdit::sizeHint(); + size.setWidth(m_nuCharactersHint * metrics.averageCharWidth()); + return size; +} + +//----------------------------------------------------------------------------- + diff --git a/src/libpentobi_gui/LineEdit.h b/src/libpentobi_gui/LineEdit.h new file mode 100644 index 0000000..3dbf421 --- /dev/null +++ b/src/libpentobi_gui/LineEdit.h @@ -0,0 +1,37 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/LineEdit.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_GUI_LINE_EDIT_H +#define LIBPENTOBI_GUI_LINE_EDIT_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +//----------------------------------------------------------------------------- + +/** QLineEdit with a configurable size hint depending on the expected + number of characters. */ +class LineEdit + : public QLineEdit +{ + Q_OBJECT + +public: + explicit LineEdit(int nuCharactersHint); + + QSize sizeHint() const override; + +private: + int m_nuCharactersHint; +}; + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_GUI_LINE_EDIT_H diff --git a/src/libpentobi_gui/OrientationDisplay.cpp b/src/libpentobi_gui/OrientationDisplay.cpp new file mode 100644 index 0000000..19dcfbd --- /dev/null +++ b/src/libpentobi_gui/OrientationDisplay.cpp @@ -0,0 +1,218 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/OrientationDisplay.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "OrientationDisplay.h" + +#include +#include "libboardgame_base/GeometryUtil.h" +#include "libpentobi_gui/Util.h" + +using namespace std; +using libboardgame_base::ArrayList; +using libboardgame_base::CoordPoint; +using libboardgame_base::Transform; +using libboardgame_base::geometry_util::normalize_offset; +using libboardgame_base::geometry_util::type_match_offset; +using libboardgame_base::geometry_util::type_match_shift; +using libpentobi_base::Geometry; +using libpentobi_base::PiecePoints; +using libpentobi_base::PieceSet; + +//----------------------------------------------------------------------------- + +OrientationDisplay::OrientationDisplay(QWidget* parent, const Board& bd) + : QWidget(parent), + m_bd(bd) +{ + setMinimumSize(30, 30); +} + +void OrientationDisplay::clearSelectedColor() +{ + if (m_isColorSelected) + { + m_isColorSelected = false; + update(); + } +} + +void OrientationDisplay::clearPiece() +{ + if (m_piece.is_null()) + return; + m_piece = Piece::null(); + update(); +} + +void OrientationDisplay::mousePressEvent(QMouseEvent*) +{ + if (m_isColorSelected && m_piece.is_null()) + emit colorClicked(m_color); +} + +void OrientationDisplay::paintEvent(QPaintEvent*) +{ + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing, true); + auto variant = m_bd.get_variant(); + qreal fieldWidth; + qreal fieldHeight; + qreal displayWidth; + qreal displayHeight; + auto pieceSet = m_bd.get_piece_set(); + bool isTrigon = (pieceSet == PieceSet::trigon); + bool isNexos = (pieceSet == PieceSet::nexos); + bool isCallisto = (pieceSet == PieceSet::callisto); + qreal ratio; + int columns; + int rows; + if (isTrigon) + { + ratio = 1.732; + columns = 7; + rows = 4; + } + else if (isNexos) + { + ratio = 1; + columns = 8; + rows = 8; + } + else + { + ratio = 1; + columns = 5; + rows = 5; + } + fieldWidth = min(qreal(width()) / columns, + qreal(height()) / (ratio * rows)); + if (fieldWidth > 8) + // Prefer pixel alignment if piece is not too small + fieldWidth = floor(fieldWidth); + fieldHeight = ratio * fieldWidth; + displayWidth = fieldWidth * columns; + displayHeight = fieldHeight * rows; + if (m_piece.is_null()) + { + if (m_isColorSelected) + { + qreal dotSize = 0.07 * height(); + QColor color = Util::getPaintColor(variant, m_color); + painter.setBrush(color); + painter.setPen(Qt::NoPen); + painter.drawEllipse(QPointF(0.5 * width(), 0.5 * height()), + dotSize, dotSize); + } + return; + } + painter.save(); + painter.translate(0.5 * (width() - displayWidth), + 0.5 * (height() - displayHeight)); + PiecePoints points = m_bd.get_piece_info(m_piece).get_points(); + m_transform->transform(points.begin(), points.end()); + auto& geo = m_bd.get_geometry(); + type_match_shift(geo, points.begin(), points.end(), + m_transform->get_new_point_type()); + unsigned width; + unsigned height; + CoordPoint offset; + normalize_offset(points.begin(), points.end(), width, height, offset); + offset = type_match_offset(geo, geo.get_point_type(offset)); + painter.save(); + painter.translate(0.5 * (displayWidth - width * fieldWidth), + 0.5 * (displayHeight - height * fieldHeight)); + ArrayList junctions; + for (CoordPoint p : points) + { + qreal x = p.x * fieldWidth; + qreal y = p.y * fieldHeight; + auto pointType = geo.get_point_type(p + offset); + if (isTrigon) + { + bool isUpward = (pointType == 0); + Util::paintColorTriangle(painter, variant, m_color, isUpward, + x, y, fieldWidth, fieldHeight); + } + else if (isNexos) + { + if (pointType == 1 || pointType == 2) + { + bool isHorizontal = (pointType == 1); + Util::paintColorSegment(painter, variant, m_color, + isHorizontal, x, y, fieldWidth); + if (pointType == 1) // Horiz. segment + { + junctions.include(CoordPoint(p.x - 1, p.y)); + junctions.include(CoordPoint(p.x + 1, p.y)); + } + else + { + LIBBOARDGAME_ASSERT(pointType == 2); // Vert. segment + junctions.include(CoordPoint(p.x, p.y - 1)); + junctions.include(CoordPoint(p.x, p.y + 1)); + } + } + } + else if (isCallisto) + { + bool hasRight = points.contains(CoordPoint(p.x + 1, p.y)); + bool hasDown = points.contains(CoordPoint(p.x, p.y + 1)); + bool isOnePiece = (points.size() == 1); + Util::paintColorSquareCallisto(painter, variant, m_color, x, y, + fieldWidth, hasRight, hasDown, + isOnePiece); + } + else + Util::paintColorSquare(painter, variant, m_color, x, y, + fieldWidth); + } + if (isNexos) + for (CoordPoint p : junctions) + { + bool hasLeft = points.contains(CoordPoint(p.x - 1, p.y)); + bool hasRight = points.contains(CoordPoint(p.x + 1, p.y)); + bool hasUp = points.contains(CoordPoint(p.x, p.y - 1)); + bool hasDown = points.contains(CoordPoint(p.x, p.y + 1)); + Util::paintJunction(painter, variant, m_color, p.x * fieldWidth, + p.y * fieldHeight, fieldWidth, fieldHeight, + hasLeft, hasRight, hasUp, hasDown); + } + painter.restore(); + painter.restore(); +} + +void OrientationDisplay::selectColor(Color c) +{ + if (m_isColorSelected && m_color == c) + return; + m_isColorSelected = true; + m_color = c; + update(); +} + +void OrientationDisplay::setSelectedPiece(Piece piece) +{ + auto transform = m_bd.get_transforms().get_default(); + if (m_piece == piece && m_transform == transform) + return; + m_piece = piece; + m_transform = transform; + update(); +} + +void OrientationDisplay::setSelectedPieceTransform(const Transform* transform) +{ + if (m_transform == transform) + return; + m_transform = transform; + update(); +} + +//----------------------------------------------------------------------------- diff --git a/src/libpentobi_gui/OrientationDisplay.h b/src/libpentobi_gui/OrientationDisplay.h new file mode 100644 index 0000000..ba28eda --- /dev/null +++ b/src/libpentobi_gui/OrientationDisplay.h @@ -0,0 +1,68 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/OrientationDisplay.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_GUI_ORIENTATION_DISPLAY_H +#define LIBPENTOBI_GUI_ORIENTATION_DISPLAY_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include "libpentobi_base/Board.h" + +using libboardgame_base::Transform; +using libpentobi_base::Board; +using libpentobi_base::Color; +using libpentobi_base::Piece; +using libpentobi_base::PieceInfo; + +//----------------------------------------------------------------------------- + +class OrientationDisplay + : public QWidget +{ + Q_OBJECT + +public: + OrientationDisplay(QWidget* parent, const Board& bd); + + void selectColor(Color c); + + void clearSelectedColor(); + + void clearPiece(); + + void setSelectedPiece(Piece piece); + + void setSelectedPieceTransform(const Transform* transform); + +signals: + /** A mouse click on the orientation display while a color but no no piece + was selected. */ + void colorClicked(Color color); + +protected: + void mousePressEvent(QMouseEvent* event) override; + + void paintEvent(QPaintEvent* event) override; + +private: + const Board& m_bd; + + Piece m_piece = Piece::null(); + + const Transform* m_transform = nullptr; + + bool m_isColorSelected = false; + + Color m_color; +}; + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_GUI_ORIENTATION_DISPLAY_H diff --git a/src/libpentobi_gui/PieceSelector.cpp b/src/libpentobi_gui/PieceSelector.cpp new file mode 100644 index 0000000..d6d3029 --- /dev/null +++ b/src/libpentobi_gui/PieceSelector.cpp @@ -0,0 +1,380 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/PieceSelector.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "PieceSelector.h" + +#include +#include +#include "libboardgame_base/GeometryUtil.h" +#include "libboardgame_util/StringUtil.h" +#include "libpentobi_gui/Util.h" + +using libboardgame_base::CoordPoint; +using libboardgame_base::geometry_util::type_match_shift; +using libboardgame_util::trim; +using libpentobi_base::BoardConst; +using libpentobi_base::BoardType; +using libpentobi_base::Geometry; +using libpentobi_base::PieceMap; +using libpentobi_base::PieceSet; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +namespace { + +const char* pieceLayoutCallisto = + " 1 . U U U . O O . O O . L L . L . Z . . Z . . I . I . 2" + " . . U . U . O O . O O . L . . L . Z Z . Z Z . I . I . 2" + " 1 . . . . . . . . . . . L . L L . . Z . . Z . I . I . ." + " . .T5T5T5 . . W . . X . . . . . . . . . . . . . . . . 2" + " 1 . .T5 . . W W . X X X .T4T4T4 .T4T4T4 . V . . V . . 2" + " . . .T5 . W W . . . X . . .T4 . . .T4 . . V V . V V . ."; + +const char* pieceLayoutClassic = + " 1 .Z4Z4 . .L4L4L4 . O O . P P .L5L5L5L5 .V5V5V5 . U U U . N . . ." + " . . .Z4Z4 . . .L4 . O O . P P .L5 . . . .V5 . . . U . U . N N .I5" + " 2 2 . . . .T4 . . . . . . P . . . . X . .V5 .Z5 . . . . . . N .I5" + " . . .I3 .T4T4T4 . . W W . . . F . X X X . . .Z5Z5Z5 . .T5 . N .I5" + "V3 . .I3 . . . . . . . W W . F F . . X . . Y . . .Z5 . .T5 . . .I5" + "V3V3 .I3 . .I4I4I4I4 . . W . . F F . . . Y Y Y Y . . .T5T5T5 . .I5"; + +const char* pieceLayoutJunior = + "1 . 1 . V3V3. . L4L4L4. T4T4T4. . O O . O O . P P . . I5. I5. . L5L5" + ". . . . V3. . . L4. . . . T4. . . O O . O O . P P . . I5. I5. . . L5" + "2 . 2 . . . V3. . . . L4. . . T4. . . . . . . P . . . I5. I5. L5. L5" + "2 . 2 . . V3V3. . L4L4L4. . T4T4T4. . Z4. Z4. . . P . I5. I5. L5. L5" + ". . . . . . . . . . . . . . . . . . Z4Z4. Z4Z4. P P . I5. I5. L5. . " + "I3I3I3. I3I3I3. I4I4I4I4. I4I4I4I4. Z4. . . Z4. P P . . . . . L5L5. "; + +const char* pieceLayoutTrigon = + "L5L5 . . F F F F . .L6L6 . . O O O . . X X X . . .A6A6 . . G G . G . .C4C4 . . Y Y Y Y" + "L5L5 . . F . F . . .L6L6 . . O O O . . X X X . .A6A6A6A6 . . G G G . .C4C4 . . Y Y . ." + " .L5 . . . . . . S . .L6L6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2" + " . . . . . S S S S . . . . . . . .P5P5P5P5 . . .I6I6 . .I5I5I5I5I5 . . W W W W W . . 2" + "C5C5 . . . S . . . . V V . .P6 . . . .P5 . .A4 . .I6I6 . . . . . . . . . . W . . . . ." + "C5C5C5 . . . . V V V V . .P6P6P6P6P6 . . .A4A4A4 . .I6I6 . .I3I3I3 . . 1 . . .I4I4I4I4"; + +// To increase the clickable area and to ensure that the pieces can be found +// in the string with flood filling, the Nexos pieces also include some +// crossable junction points that are not part of the piece definition(they +// will be filtered out before finding the piece). But the number of points per +// piece must be at most PiecePoints::max_size. +const char* pieceLayoutNexos = + " . . F F F F F . . . O O O .U4U4U4U4U4 . . . . N N N N . . . . H H H . .U3 .U3 . . .V2V2V2" + "I4 . . . F . F . Y . O . O .U4 . . .U4 .T4 . . . . . N . . . . . H . . .U3 .U3 . . . . .V2" + "I4 . . . . . . . Y . . O O . . . . . . .T4T4T4T4 . . N N . . . . H H . .U3U3U3 . . . . .V2" + "I4 .L4 . . . . . Y . . . . . . . . . . .T4 . . . . . . . . . X . . . . . . . . . . . J . ." + "I4 .L4 . . . . Y Y .L3 . G G . . . . . . . . . . .Z3Z3 . . X X X . . . . . .I2 . . . J . ." + "I4 .L4 . W . . . Y .L3 . G . . . E . . . . .T3 . . .Z3 . . . X . . . . .Z4 .I2 . J . J .V4" + "I4 .L4 . W W W . Y .L3 . G G G . E E E E . .T3T3 . .Z3Z3 . . . .Z4Z4Z4Z4Z4 .I2 . J J J .V4" + "I4 .L4 . . . W . . .L3 . . . G . . . E . . .T3 . . . . . . . . .Z4 . . . . .I2 . . . . .V4" + " . .L4L4 . . W W . .L3L3L3 . . . . . . . . . . . .I3I3I3I3I3 . . . . 1 1 1 .I2 . .V4V4V4V4"; + +} // namespace + +//----------------------------------------------------------------------------- + +PieceSelector::PieceSelector(QWidget* parent, const Board& bd, Color color) + : QWidget(parent), + m_bd(bd), + m_color(color) +{ + setMinimumSize(170, 30); + init(); +} + +void PieceSelector::checkUpdate() +{ + bool disabledStatus[maxColumns][maxRows]; + setDisabledStatus(disabledStatus); + for (unsigned x = 0; x < m_nuColumns; ++x) + for (unsigned y = 0; y < m_nuRows; ++y) + if (! m_piece[x][y].is_null() + && disabledStatus[x][y] != m_disabled[x][y]) + { + update(); + return; + } +} + +void PieceSelector::filterCrossableJunctions(PiecePoints& points) const +{ + auto& geo = m_bd.get_geometry(); + PiecePoints newPoints; + for (auto& p : points) + { + if (geo.get_point_type(p) != 0) + // Not a junction + newPoints.push_back(p); + else if (points.contains(CoordPoint(p.x - 1, p.y)) + && points.contains(CoordPoint(p.x + 1, p.y)) + && ! points.contains(CoordPoint(p.x, p.y - 1)) + && ! points.contains(CoordPoint(p.x, p.y + 1))) + // Necessary junction + newPoints.push_back(p); + else if (! points.contains(CoordPoint(p.x - 1, p.y)) + && ! points.contains(CoordPoint(p.x + 1, p.y)) + && points.contains(CoordPoint(p.x, p.y - 1)) + && points.contains(CoordPoint(p.x, p.y + 1))) + // Necessary junction + newPoints.push_back(p); + } + points = newPoints; +} + +void PieceSelector::findPiecePoints(Piece piece, unsigned x, unsigned y, + PiecePoints& points) const +{ + CoordPoint p(x, y); + if (x >= m_nuColumns || y >= m_nuRows || m_piece[x][y] != piece + || points.contains(p)) + return; + points.push_back(p); + // This assumes that no Trigon pieces touch at the corners, otherwise + // we would need to iterate over neighboring CoordPoint's corresponding to + // Geometry::get_adj() + findPiecePoints(piece, x + 1, y, points); + findPiecePoints(piece, x - 1, y, points); + findPiecePoints(piece, x, y + 1, points); + findPiecePoints(piece, x, y - 1, points); +} + +void PieceSelector::init() +{ + auto pieceSet = m_bd.get_piece_set(); + switch (pieceSet) + { + case PieceSet::classic: + m_pieceLayout = pieceLayoutClassic; + m_nuColumns = 33; + m_nuRows = 6; + break; + case PieceSet::trigon: + m_pieceLayout = pieceLayoutTrigon; + m_nuColumns = 43; + m_nuRows = 6; + break; + case PieceSet::junior: + m_pieceLayout = pieceLayoutJunior; + m_nuColumns = 34; + m_nuRows = 6; + break; + case PieceSet::nexos: + m_pieceLayout = pieceLayoutNexos; + m_nuColumns = 45; + m_nuRows = 9; + break; + case PieceSet::callisto: + m_pieceLayout = pieceLayoutCallisto; + m_nuColumns = 28; + m_nuRows = 6; + break; + } + LIBBOARDGAME_ASSERT(m_nuColumns <= maxColumns); + LIBBOARDGAME_ASSERT(m_nuRows <= maxRows); + for (unsigned y = 0; y < m_nuRows; ++y) + for (unsigned x = 0; x < m_nuColumns; ++x) + { + string name = m_pieceLayout.substr(y * m_nuColumns * 2 + x * 2, 2); + name = trim(name); + Piece piece = Piece::null(); + if (name != ".") + { + m_bd.get_piece_by_name(name, piece); + LIBBOARDGAME_ASSERT(! piece.is_null()); + } + m_piece[x][y] = piece; + } + auto& geo = m_bd.get_geometry(); + for (unsigned y = 0; y < m_nuRows; ++y) + for (unsigned x = 0; x < m_nuColumns; ++x) + { + Piece piece = m_piece[x][y]; + if (piece.is_null()) + continue; + PiecePoints points; + findPiecePoints(piece, x, y, points); + // We need to match the coordinate system of the piece selector to + // the geometry, they are different in Trigon3. + type_match_shift(geo, points.begin(), points.end(), 0); + if (pieceSet == PieceSet::nexos) + filterCrossableJunctions(points); + m_transform[x][y] = + m_bd.get_piece_info(piece).find_transform(geo, points); + LIBBOARDGAME_ASSERT(m_transform[x][y]); + } + setDisabledStatus(m_disabled); + update(); +} + +void PieceSelector::mousePressEvent(QMouseEvent* event) +{ + qreal pixelX = event->x() - 0.5 * (width() - m_selectorWidth); + qreal pixelY = event->y() - 0.5 * (height() - m_selectorHeight); + if (pixelX < 0 || pixelX >= m_selectorWidth + || pixelY < 0 || pixelY >= m_selectorHeight) + return; + int x = static_cast(pixelX / m_fieldWidth); + int y = static_cast(pixelY / m_fieldHeight); + Piece piece = m_piece[x][y]; + if (piece.is_null() || m_disabled[x][y]) + return; + update(); + emit pieceSelected(m_color, piece, m_transform[x][y]); +} + +void PieceSelector::paintEvent(QPaintEvent*) +{ + setDisabledStatus(m_disabled); + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing, true); + auto pieceSet = m_bd.get_piece_set(); + bool isTrigon = (pieceSet == PieceSet::trigon); + bool isNexos = (pieceSet == PieceSet::nexos); + bool isCallisto = (pieceSet == PieceSet::callisto); + qreal ratio; + if (isTrigon) + { + ratio = 1.732; + m_fieldWidth = min(qreal(width()) / (m_nuColumns + 1), + qreal(height()) / (ratio * m_nuRows)); + } + else + { + ratio = 1; + m_fieldWidth = min(qreal(width()) / m_nuColumns, + qreal(height()) / m_nuRows); + } + if (m_fieldWidth > 8) + // Prefer pixel alignment if piece is not too small + m_fieldWidth = floor(m_fieldWidth); + m_fieldHeight = ratio * m_fieldWidth; + m_selectorWidth = m_fieldWidth * m_nuColumns; + m_selectorHeight = m_fieldHeight * m_nuRows; + painter.save(); + painter.translate(0.5 * (width() - m_selectorWidth), + 0.5 * (height() - m_selectorHeight)); + auto variant = m_bd.get_variant(); + auto& geo = m_bd.get_geometry(); + for (unsigned x = 0; x < m_nuColumns; ++x) + for (unsigned y = 0; y < m_nuRows; ++y) + { + auto pointType = geo.get_point_type(x, y); + Piece piece = m_piece[x][y]; + if (isTrigon) + { + if (piece.is_null() || m_disabled[x][y]) + continue; + bool isUpward = (pointType == geo.get_point_type(0, 0)); + Util::paintColorTriangle(painter, variant, m_color, isUpward, + x * m_fieldWidth, y * m_fieldHeight, + m_fieldWidth, m_fieldHeight); + } + else if (isNexos) + { + if (pointType == 1 || pointType == 2) + { + if (piece.is_null() || m_disabled[x][y]) + continue; + bool isHorizontal = (geo.get_point_type(x, y) == 1); + Util::paintColorSegment(painter, variant, m_color, + isHorizontal, x * m_fieldWidth, + y * m_fieldHeight, m_fieldWidth); + } + else if (pointType == 0) + { + bool hasLeft = + (x > 0 && ! m_piece[x - 1][y].is_null() + && ! m_disabled[x - 1][y]); + bool hasRight = + (x < m_nuColumns - 1 + && ! m_piece[x + 1][y].is_null() + && ! m_disabled[x + 1][y]); + bool hasUp = + (y > 0 && ! m_piece[x][y - 1].is_null() + && ! m_disabled[x][y - 1]); + bool hasDown = + (y < m_nuRows - 1 + && ! m_piece[x][y + 1].is_null() + && ! m_disabled[x][y + 1]); + Util::paintJunction(painter, variant, m_color, + x * m_fieldWidth, y * m_fieldHeight, + m_fieldWidth, m_fieldHeight, hasLeft, + hasRight, hasUp, hasDown); + } + } + else + { + if (piece.is_null() || m_disabled[x][y]) + continue; + if (isCallisto) + { + bool hasLeft = (x > 0 && ! m_piece[x - 1][y].is_null()); + bool hasRight = + (x < m_nuColumns - 1 + && ! m_piece[x + 1][y].is_null()); + bool hasUp = (y > 0 && ! m_piece[x][y - 1].is_null()); + bool hasDown = + (y < m_nuRows - 1 + && ! m_piece[x][y + 1].is_null()); + bool isOnePiece = + (! hasLeft && ! hasRight && ! hasUp && ! hasDown); + Util::paintColorSquareCallisto(painter, variant, m_color, + x * m_fieldWidth, + y * m_fieldHeight, + m_fieldWidth, hasRight, + hasDown, isOnePiece); + } + else + Util::paintColorSquare(painter, variant, m_color, + x * m_fieldWidth, y * m_fieldHeight, + m_fieldWidth); + } + } + painter.restore(); +} + +void PieceSelector::setDisabledStatus(bool disabledStatus[maxColumns][maxRows]) +{ + bool marker[maxColumns][maxRows]; + for (unsigned x = 0; x < m_nuColumns; ++x) + for (unsigned y = 0; y < m_nuRows; ++y) + { + marker[x][y] = false; + disabledStatus[x][y] = false; + } + PieceMap nuInstances; + nuInstances.fill(0); + bool isColorUsed = (m_color.to_int() < m_bd.get_nu_colors()); + for (unsigned x = 0; x < m_nuColumns; ++x) + for (unsigned y = 0; y < m_nuRows; ++y) + { + if (marker[x][y]) + continue; + Piece piece = m_piece[x][y]; + if (piece.is_null()) + continue; + PiecePoints points; + findPiecePoints(piece, x, y, points); + bool disabled = false; + if (! isColorUsed + || ++nuInstances[piece] > m_bd.get_nu_left_piece(m_color, + piece)) + disabled = true; + for (auto& p : points) + { + disabledStatus[p.x][p.y] = disabled; + marker[p.x][p.y] = true; + } + } +} + +//----------------------------------------------------------------------------- diff --git a/src/libpentobi_gui/PieceSelector.h b/src/libpentobi_gui/PieceSelector.h new file mode 100644 index 0000000..2f49ebc --- /dev/null +++ b/src/libpentobi_gui/PieceSelector.h @@ -0,0 +1,94 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/PieceSelector.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_GUI_PIECE_SELECTOR_H +#define LIBPENTOBI_GUI_PIECE_SELECTOR_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include "libpentobi_base/Board.h" +#include "libpentobi_base/Color.h" + +using namespace std; +using libboardgame_base::Transform; +using libboardgame_util::ArrayList; +using libpentobi_base::Color; +using libpentobi_base::Board; +using libpentobi_base::Piece; +using libpentobi_base::PiecePoints; + +//----------------------------------------------------------------------------- + +class PieceSelector + : public QWidget +{ + Q_OBJECT + +public: + PieceSelector(QWidget* parent, const Board& bd, Color color); + + /** Needs to be called after the game variant of the current board has + changed because references to pieces are only unique within a + game variant. */ + void init(); + + /** Call update() if pieces left have changed since last paint. */ + void checkUpdate(); + +signals: + void pieceSelected(Color color, Piece piece, const Transform* transform); + +protected: + void mousePressEvent(QMouseEvent* event) override; + + void paintEvent(QPaintEvent* event) override; + +private: + static const unsigned maxColumns = 45; + + static const unsigned maxRows = 9; + + const Board& m_bd; + + Color m_color; + + unsigned m_nuColumns; + + unsigned m_nuRows; + + Piece m_piece[maxColumns][maxRows]; + + const Transform* m_transform[maxColumns][maxRows]; + + bool m_disabled[maxColumns][maxRows]; + + qreal m_fieldWidth; + + qreal m_fieldHeight; + + qreal m_selectorWidth; + + qreal m_selectorHeight; + + string m_pieceLayout; + + + void filterCrossableJunctions(PiecePoints& points) const; + + void findPiecePoints(Piece piece, unsigned x, unsigned y, + PiecePoints& points) const; + + void setDisabledStatus(bool disabledStatus[maxColumns][maxRows]); +}; + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_GUI_PIECE_SELECTOR_H diff --git a/src/libpentobi_gui/SameHeightLayout.cpp b/src/libpentobi_gui/SameHeightLayout.cpp new file mode 100644 index 0000000..b61d887 --- /dev/null +++ b/src/libpentobi_gui/SameHeightLayout.cpp @@ -0,0 +1,116 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/SameHeightLayout.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "SameHeightLayout.h" + +#include +#include + +using namespace std; + +//----------------------------------------------------------------------------- + +SameHeightLayout::SameHeightLayout(QWidget* parent) + : QLayout(parent) +{ +} + +SameHeightLayout::~SameHeightLayout() +{ + QLayoutItem* item; + while ((item = takeAt(0))) + delete item; +} + +void SameHeightLayout::addItem(QLayoutItem* item) +{ + m_list.append(item); +} + +QSize SameHeightLayout::sizeHint() const +{ + QSize s(0, 0); + int count = m_list.count(); + int i = 0; + while (i < count) + { + QSize size = m_list.at(i)->sizeHint(); + s.setWidth(max(size.width(), s.width())); + s.setHeight(s.height() + size.height()); + ++i; + } + return s + (count - 1) * QSize(0, getSpacing()); +} + +QSize SameHeightLayout::minimumSize() const +{ + QSize s(0, 0); + int count = m_list.count(); + int i = 0; + while (i < count) + { + QSize size = m_list.at(i)->minimumSize(); + s.setWidth(max(size.width(), s.width())); + s.setHeight(s.height() + size.height()); + ++i; + } + return s + (count - 1) * QSize(0, getSpacing()); +} + +int SameHeightLayout::count() const +{ + return m_list.size(); +} + +int SameHeightLayout::getSpacing() const +{ + // spacing() returns -1 with Qt 4.7 on KDE. It returns 6 on Gnome. Is this a + // bug? The documentation says: "If no value is explicitly set, the layout's + // spacing is inherited from the parent layout, or from the style settings + // for the parent widget." + int result = spacing(); + if (result < 0 && parentWidget()) + result = parentWidget()->style()->layoutSpacing(QSizePolicy::Frame, + QSizePolicy::Frame, + Qt::Vertical); + if (result < 0) + result = 5; + return result; +} + +QLayoutItem* SameHeightLayout::itemAt(int i) const +{ + return m_list.value(i); +} + +QLayoutItem* SameHeightLayout::takeAt(int i) +{ + return i >= 0 && i < m_list.size() ? m_list.takeAt(i) : nullptr; +} + +void SameHeightLayout::setGeometry(const QRect& rect) +{ + QLayout::setGeometry(rect); + if (m_list.size() == 0) + return; + int count = m_list.count(); + int width = rect.width(); + int height = (rect.height() - (count - 1) * getSpacing()) / count; + int x = rect.x(); + int y = rect.y(); + for (int i = 0; i < count; ++i) + { + QRect geom(x, y, width, height); + m_list.at(i)->setGeometry(geom); + y = y + height + getSpacing(); + } +} + +//----------------------------------------------------------------------------- diff --git a/src/libpentobi_gui/SameHeightLayout.h b/src/libpentobi_gui/SameHeightLayout.h new file mode 100644 index 0000000..e2bf5ff --- /dev/null +++ b/src/libpentobi_gui/SameHeightLayout.h @@ -0,0 +1,55 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/SameHeightLayout.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_GUI_SAME_HEIGHT_LAYOUT_H +#define LIBPENTOBI_GUI_SAME_HEIGHT_LAYOUT_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +//----------------------------------------------------------------------------- + +/** Layout that assigns exactly the same height to all items. + Needed for the box containing the piece selectors, because QBoxLayout + and QGridLayout do not always assign the exact same height to all items + if the height is not a multiple of the number of items. */ +class SameHeightLayout + : public QLayout +{ + Q_OBJECT + +public: + explicit SameHeightLayout(QWidget* parent = nullptr); + + ~SameHeightLayout(); + + void addItem(QLayoutItem* item) override; + + QSize sizeHint() const override; + + QSize minimumSize() const override; + + int count() const override; + + QLayoutItem* itemAt(int i) const override; + + QLayoutItem* takeAt(int i) override; + + void setGeometry(const QRect& rect) override; + +private: + QList m_list; + + int getSpacing() const; +}; + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_GUI_SAME_HEIGHT_LAYOUT_H diff --git a/src/libpentobi_gui/ScoreDisplay.cpp b/src/libpentobi_gui/ScoreDisplay.cpp new file mode 100644 index 0000000..e6208f4 --- /dev/null +++ b/src/libpentobi_gui/ScoreDisplay.cpp @@ -0,0 +1,314 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/ScoreDisplay.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "ScoreDisplay.h" + +#include +#include +#include +#include "libpentobi_gui/Util.h" + +using namespace std; + +//----------------------------------------------------------------------------- + +ScoreDisplay::ScoreDisplay(QWidget* parent) + : QWidget(parent) +{ + m_variant = Variant::classic; + m_font.setStyleStrategy(QFont::StyleStrategy(QFont::PreferOutline + | QFont::PreferQuality)); + setMinimumSize(300, 20); + setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); +} + +void ScoreDisplay::drawScore(QPainter& painter, Color c, int x) +{ + QColor color = Util::getPaintColor(m_variant, c); + painter.setPen(Qt::NoPen); + painter.setBrush(color); + QFontMetrics metrics(m_font); + int ascent = metrics.ascent(); + // y is baseline + int y = static_cast(ceil(0.5 * (height() - ascent)) + ascent); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.drawEllipse(x, y - m_colorDotSize, m_colorDotSize, + m_colorDotSize); + QString text = getScoreText(c); + bool underline = ! m_hasMoves[c]; + bool hasBonus = m_bonus[c] != 0; + drawText(painter, text, x + m_colorDotWidth, y, underline, hasBonus); +} + +void ScoreDisplay::drawScore2(QPainter& painter, Color c1, Color c2, int x) +{ + auto color = Util::getPaintColor(m_variant, c1); + painter.setPen(Qt::NoPen); + painter.setBrush(color); + QFontMetrics metrics(m_font); + int ascent = metrics.ascent(); + // y is baseline + int y = static_cast(ceil(0.5 * (height() - ascent)) + ascent); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.drawEllipse(x, y - m_colorDotSize, m_colorDotSize, m_colorDotSize); + color = Util::getPaintColor(m_variant, c2); + painter.setBrush(color); + painter.drawEllipse(x + m_colorDotSize, y - m_colorDotSize, m_colorDotSize, + m_colorDotSize); + QString text = getScoreText2(c1, c2); + bool underline = (! m_hasMoves[c1] && ! m_hasMoves[c2]); + drawText(painter, text, x + m_twoColorDotWidth, y, underline, false); +} + +void ScoreDisplay::drawScore3(QPainter& painter, int x) +{ + auto color = Util::getPaintColor(m_variant, Color(3)); + painter.setPen(Qt::NoPen); + painter.setBrush(color); + QFontMetrics metrics(m_font); + int ascent = metrics.ascent(); + // y is baseline + int y = static_cast(ceil(0.5 * (height() - ascent)) + ascent); + painter.setRenderHint(QPainter::Antialiasing, true); + if (m_hasMoves[Color(3)]) + { + painter.drawEllipse(x, y - m_colorDotSize, + m_colorDotSize, m_colorDotSize); + color = Util::getPaintColor(m_variant, m_altPlayer); + painter.setBrush(color); + painter.drawEllipse(x + m_colorDotSize, y - m_colorDotSize, + m_colorDotSize, m_colorDotSize); + } + else + painter.drawEllipse(x + m_colorDotSize, y - m_colorDotSize, + m_colorDotSize, m_colorDotSize); + QString text = getScoreText3(); + bool underline = ! m_hasMoves[Color(3)]; + drawText(painter, text, x + m_twoColorDotWidth, y, underline, false); +} + +void ScoreDisplay::drawText(QPainter& painter, const QString& text, int x, + int y, bool underline, bool hasBonus) +{ + painter.setFont(m_font); + QFontMetrics metrics(m_font); + auto color = QApplication::palette().color(QPalette::WindowText); + painter.setPen(color); + painter.setRenderHint(QPainter::Antialiasing, false); + painter.drawText(x, y, text); + if (underline) + { + // Draw underline (instead of using an underlined font because the + // underline of some fonts is too close to the text and we want it + // to be very visible) + int lineWidth = metrics.lineWidth(); + QPen pen(color); + pen.setWidth(lineWidth); + painter.setPen(pen); + y += 2 * lineWidth; + if (y > height() - 1) + y = height() - 1; + painter.drawLine(x + (hasBonus ? metrics.width(text.left(1)) : 0), y, + x + metrics.width(text), y); + } +} + +QString ScoreDisplay::getScoreText(ScoreType points, ScoreType bonus) const +{ + return QString("%1%2").arg(bonus > 0 ? "*" : "", QString::number(points)); +} + +QString ScoreDisplay::getScoreText(Color c) +{ + return getScoreText(m_points[c], m_bonus[c]); +} + +QString ScoreDisplay::getScoreText2(Color c1, Color c2) +{ + return getScoreText(m_points[c1] + m_points[c2], 0); +} + +QString ScoreDisplay::getScoreText3() +{ + return "(" + getScoreText(Color(3)) + ")"; +} + +int ScoreDisplay::getScoreTextWidth(Color c) +{ + return getTextWidth(getScoreText(c)); +} + +int ScoreDisplay::getScoreTextWidth2(Color c1, Color c2) +{ + return getTextWidth(getScoreText2(c1, c2)); +} + +int ScoreDisplay::getScoreTextWidth3() +{ + return getTextWidth(getScoreText3()); +} + +int ScoreDisplay::getTextWidth(QString text) const +{ + // Make text width only depend on number of digits to avoid frequent small + // changes to the layout + QFontMetrics metrics(m_font); + int maxDigitWidth = 0; + maxDigitWidth = max(maxDigitWidth, metrics.width('0')); + maxDigitWidth = max(maxDigitWidth, metrics.width('1')); + maxDigitWidth = max(maxDigitWidth, metrics.width('2')); + maxDigitWidth = max(maxDigitWidth, metrics.width('3')); + maxDigitWidth = max(maxDigitWidth, metrics.width('4')); + maxDigitWidth = max(maxDigitWidth, metrics.width('5')); + maxDigitWidth = max(maxDigitWidth, metrics.width('6')); + maxDigitWidth = max(maxDigitWidth, metrics.width('7')); + maxDigitWidth = max(maxDigitWidth, metrics.width('8')); + maxDigitWidth = max(maxDigitWidth, metrics.width('9')); + return max(text.length() * maxDigitWidth, + metrics.boundingRect(text).width()); +} + +void ScoreDisplay::paintEvent(QPaintEvent*) +{ + QPainter painter(this); + m_colorDotSize = static_cast(0.8 * m_fontSize); + m_colorDotSpace = static_cast(0.3 * m_fontSize); + m_colorDotWidth = m_colorDotSize + m_colorDotSpace; + m_twoColorDotWidth = 2 * m_colorDotSize + m_colorDotSpace; + auto nuColors = get_nu_colors(m_variant); + auto nuPlayers = get_nu_players(m_variant); + if (nuColors == 2) + { + int textWidthBlue = getScoreTextWidth(Color(0)); + int textWidthGreen = getScoreTextWidth(Color(1)); + int totalWidth = textWidthBlue + textWidthGreen + 2 * m_colorDotWidth; + qreal pad = qreal(width() - totalWidth) / 3.f; + qreal x = pad; + drawScore(painter, Color(0), static_cast(x)); + x += m_colorDotWidth + textWidthBlue + pad; + drawScore(painter, Color(1), static_cast(x)); + } + else if (nuColors == 4 && nuPlayers == 4) + { + int textWidthBlue = getScoreTextWidth(Color(0)); + int textWidthYellow = getScoreTextWidth(Color(1)); + int textWidthRed = getScoreTextWidth(Color(2)); + int textWidthGreen = getScoreTextWidth(Color(3)); + int totalWidth = + textWidthBlue + textWidthRed + textWidthYellow + textWidthGreen + + 4 * m_colorDotWidth; + qreal pad = qreal(width() - totalWidth) / 5.f; + qreal x = pad; + drawScore(painter, Color(0), static_cast(x)); + x += m_colorDotWidth + textWidthBlue + pad; + drawScore(painter, Color(1), static_cast(x)); + x += m_colorDotWidth + textWidthYellow + pad; + drawScore(painter, Color(2), static_cast(x)); + x += m_colorDotWidth + textWidthRed + pad; + drawScore(painter, Color(3), static_cast(x)); + } + else if (nuColors == 4 && nuPlayers == 3) + { + int textWidthBlue = getScoreTextWidth(Color(0)); + int textWidthYellow = getScoreTextWidth(Color(1)); + int textWidthRed = getScoreTextWidth(Color(2)); + int textWidthGreen = getScoreTextWidth3(); + int totalWidth = + textWidthBlue + textWidthRed + textWidthYellow + textWidthGreen + + 3 * m_colorDotWidth + m_twoColorDotWidth; + qreal pad = qreal(width() - totalWidth) / 5.f; + qreal x = pad; + drawScore(painter, Color(0), static_cast(x)); + x += m_colorDotWidth + textWidthBlue + pad; + drawScore(painter, Color(1), static_cast(x)); + x += m_colorDotWidth + textWidthYellow + pad; + drawScore(painter, Color(2), static_cast(x)); + x += m_colorDotWidth + textWidthRed + pad; + drawScore3(painter, static_cast(x)); + } + else if (nuColors == 3 && nuPlayers == 3) + { + int textWidthBlue = getScoreTextWidth(Color(0)); + int textWidthYellow = getScoreTextWidth(Color(1)); + int textWidthRed = getScoreTextWidth(Color(2)); + int totalWidth = + textWidthBlue + textWidthRed + textWidthYellow + + 3 * m_colorDotWidth; + qreal pad = qreal(width() - totalWidth) / 4.f; + qreal x = pad; + drawScore(painter, Color(0), static_cast(x)); + x += m_colorDotWidth + textWidthBlue + pad; + drawScore(painter, Color(1), static_cast(x)); + x += m_colorDotWidth + textWidthYellow + pad; + drawScore(painter, Color(2), static_cast(x)); + } + else + { + LIBBOARDGAME_ASSERT(nuColors == 4 && nuPlayers == 2); + int textWidthBlueRed = getScoreTextWidth2(Color(0), Color(2)); + int textWidthYellowGreen = getScoreTextWidth2(Color(1), Color(3)); + int textWidthBlue = getScoreTextWidth(Color(0)); + int textWidthYellow = getScoreTextWidth(Color(1)); + int textWidthRed = getScoreTextWidth(Color(2)); + int textWidthGreen = getScoreTextWidth(Color(3)); + int totalWidth = + textWidthBlueRed + textWidthYellowGreen + + textWidthBlue + textWidthRed + textWidthYellow + textWidthGreen + + 2 * m_twoColorDotWidth + 4 * m_colorDotWidth; + qreal pad = qreal(width() - totalWidth) / 7.f; + qreal x = pad; + drawScore2(painter, Color(0), Color(2), static_cast(x)); + x += m_twoColorDotWidth + textWidthBlueRed + pad; + drawScore2(painter, Color(1), Color(3), static_cast(x)); + x += m_twoColorDotWidth + textWidthYellowGreen + pad; + drawScore(painter, Color(0), static_cast(x)); + x += m_colorDotWidth + textWidthBlue + pad; + drawScore(painter, Color(1), static_cast(x)); + x += m_colorDotWidth + textWidthYellow + pad; + drawScore(painter, Color(2), static_cast(x)); + x += m_colorDotWidth + textWidthRed + pad; + drawScore(painter, Color(3), static_cast(x)); + } +} + +void ScoreDisplay::resizeEvent(QResizeEvent*) +{ + // QFont::setPixelSize(0) prints a warning even if it works and the docs + // of Qt 5.3 don't forbid it (unlike QFont::setPointSize(0)). + m_fontSize = max(1, static_cast(floor(0.6 * height()))); + m_font.setPixelSize(m_fontSize); +} + +void ScoreDisplay::updateScore(const Board& bd) +{ + auto variant = bd.get_variant(); + bool hasChanged = (m_variant != variant); + m_variant = variant; + for (Color c : bd.get_colors()) + { + bool hasMoves = bd.has_moves(c); + auto points = bd.get_points(c); + auto bonus = bd.get_bonus(c); + if (hasMoves != m_hasMoves[c] || m_points[c] != points + || m_bonus[c] != points) + { + hasChanged = true; + m_hasMoves[c] = hasMoves; + m_points[c] = points; + m_bonus[c] = bonus; + } + } + if (variant == Variant::classic_3) + m_altPlayer = Color(bd.get_alt_player()); + if (hasChanged) + update(); +} + +//----------------------------------------------------------------------------- diff --git a/src/libpentobi_gui/ScoreDisplay.h b/src/libpentobi_gui/ScoreDisplay.h new file mode 100644 index 0000000..19e2861 --- /dev/null +++ b/src/libpentobi_gui/ScoreDisplay.h @@ -0,0 +1,94 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/ScoreDisplay.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_GUI_SCORE_DISPLAY_H +#define LIBPENTOBI_GUI_SCORE_DISPLAY_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include "libpentobi_base/Board.h" + +using libpentobi_base::Board; +using libpentobi_base::Color; +using libpentobi_base::ColorMap; +using libpentobi_base::ScoreType; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +class ScoreDisplay + : public QWidget +{ + Q_OBJECT + +public: + explicit ScoreDisplay(QWidget* parent = nullptr); + + void updateScore(const Board& bd); + +protected: + void paintEvent(QPaintEvent* event) override; + + void resizeEvent(QResizeEvent* event) override; + +private: + int m_fontSize; + + QFont m_font; + + Variant m_variant; + + ColorMap m_hasMoves{false}; + + ColorMap m_points{0}; + + ColorMap m_bonus{0}; + + /** Current player of 4th color in Variant::classic_3. */ + Color m_altPlayer; + + int m_colorDotSize; + + int m_colorDotSpace; + + int m_colorDotWidth; + + int m_twoColorDotWidth; + + + QString getScoreText(Color c); + + QString getScoreText2(Color c1, Color c2); + + QString getScoreText3(); + + int getScoreTextWidth(Color c); + + int getScoreTextWidth2(Color c1, Color c2); + + int getScoreTextWidth3(); + + void drawScore(QPainter& painter, Color c, int x); + + void drawScore2(QPainter& painter, Color c1, Color c2, int x); + + void drawScore3(QPainter& painter, int x); + + QString getScoreText(ScoreType points, ScoreType bonus) const; + + int getTextWidth(QString text) const; + + void drawText(QPainter& painter, const QString& text, int x, int y, + bool underline, bool hasBonus); +}; + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_GUI_SCORE_DISPLAY_H diff --git a/src/libpentobi_gui/Util.cpp b/src/libpentobi_gui/Util.cpp new file mode 100644 index 0000000..aad3a2f --- /dev/null +++ b/src/libpentobi_gui/Util.cpp @@ -0,0 +1,531 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/Util.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Util.h" + +#include + +//----------------------------------------------------------------------------- + +namespace { + +const QColor blue(0, 115, 207); + +const QColor green(0, 192, 0); + +const QColor red(230, 62, 44); + +const QColor yellow(235, 205, 35); + +const QColor gray(174, 167, 172); + +void setAlphaSaturation(QColor& c, qreal alpha, qreal saturation) +{ + if (saturation != 1) + c.setHsv(c.hue(), static_cast(saturation * c.saturation()), + c.value()); + if (alpha != 1) + c.setAlphaF(alpha); +} + +void paintDot(QPainter& painter, QColor color, qreal x, qreal y, qreal width, + qreal height, qreal size) +{ + painter.save(); + painter.translate(x, y); + painter.setPen(Qt::NoPen); + painter.setBrush(color); + painter.drawEllipse(QPointF(0.5 * width, 0.5 * height), size, size); + painter.restore(); +} + +void paintSquare(QPainter& painter, qreal x, qreal y, qreal width, + qreal height, const QColor& rectColor, + const QColor& upLeftColor, const QColor& downRightColor, + bool onlyBorder = false) +{ + painter.save(); + painter.translate(x, y); + if (! onlyBorder) + painter.fillRect(QRectF(0, 0, width, height), rectColor); + qreal border = 0.05 * max(width, height); + const QPointF downRightPolygon[6] = + { + QPointF(border, height - border), + QPointF(width - border, height - border), + QPointF(width - border, border), + QPointF(width, 0), + QPointF(width, height), + QPointF(0, height) + }; + painter.setPen(Qt::NoPen); + painter.setBrush(downRightColor); + painter.drawPolygon(downRightPolygon, 6); + const QPointF upLeftPolygon[6] = + { + QPointF(0, 0), + QPointF(width, 0), + QPointF(width - border, border), + QPointF(border, border), + QPointF(border, height - border), + QPointF(0, height) + }; + painter.setBrush(upLeftColor); + painter.drawPolygon(upLeftPolygon, 6); + painter.restore(); +} + +void paintTriangle(QPainter& painter, bool isUpward, qreal x, qreal y, + qreal width, qreal height, const QColor& color, + const QColor& upLeftColor, const QColor& downRightColor) +{ + painter.save(); + painter.translate(x, y); + qreal left = -0.5 * width; + qreal right = 1.5 * width; + if (isUpward) + { + const QPointF polygon[3] = + { + QPointF(left, height), + QPointF(right, height), + QPointF(0.5 * width, 0) + }; + painter.setPen(Qt::NoPen); + painter.setBrush(color); + painter.drawConvexPolygon(polygon, 3); + qreal border = 0.08 * width; + const QPointF downRightPolygon[6] = + { + QPointF(left, height), + QPointF(right, height), + QPointF(0.5 * width, 0), + QPointF(0.5 * width, 2 * border), + QPointF(right - 1.732 * border, height - border), + QPointF(left + 1.732 * border, height - border) + }; + painter.setBrush(downRightColor); + painter.drawPolygon(downRightPolygon, 6); + const QPointF upLeftPolygon[4] = + { + QPointF(0.5 * width, 0), + QPointF(0.5 * width, 2 * border), + QPointF(left + 1.732 * border, height - border), + QPointF(left, height), + }; + painter.setBrush(upLeftColor); + painter.drawPolygon(upLeftPolygon, 4); + } + else + { + const QPointF polygon[3] = + { + QPointF(left, 0), + QPointF(right, 0), + QPointF(0.5 * width, height) + }; + painter.setPen(Qt::NoPen); + painter.setBrush(color); + painter.drawConvexPolygon(polygon, 3); + qreal border = 0.05 * width; + const QPointF downRightPolygon[4] = + { + QPointF(0.5 * width, height), + QPointF(0.5 * width, height - 2 * border), + QPointF(right - 1.732 * border, border), + QPointF(right, 0) + }; + painter.setBrush(downRightColor); + painter.drawPolygon(downRightPolygon, 4); + const QPointF upLeftPolygon[6] = + { + QPointF(right, 0), + QPointF(right - 1.732 * border, border), + QPointF(left + 1.732 * border, border), + QPointF(0.5 * width, height - 2 * border), + QPointF(0.5 * width, height), + QPointF(left, 0) + }; + painter.setBrush(upLeftColor); + painter.drawPolygon(upLeftPolygon, 6); + } + painter.restore(); +} + +void paintSquareFrame(QPainter& painter, qreal x, qreal y, qreal size, + const QColor& rectColor, const QColor& upLeftColor, + const QColor& downRightColor) +{ + painter.save(); + painter.translate(x, y); + painter.setPen(Qt::NoPen); + qreal border = 0.05 * size; + qreal frameSize = 0.17 * size; + painter.fillRect(QRectF(0, 0, size, frameSize), rectColor); + painter.fillRect(QRectF(0, size - frameSize, size, frameSize), rectColor); + painter.fillRect(QRectF(0, 0, frameSize, size), rectColor); + painter.fillRect(QRectF(size - frameSize, 0, frameSize, size), rectColor); + const QPointF downRightPolygon[6] = + { + QPointF(border, size - border), + QPointF(size - border, size - border), + QPointF(size - border, border), + QPointF(size, 0), + QPointF(size, size), + QPointF(0, size) + }; + painter.setBrush(downRightColor); + painter.drawPolygon(downRightPolygon, 6); + const QPointF upLeftPolygon[6] = + { + QPointF(0, 0), + QPointF(size, 0), + QPointF(size - border, border), + QPointF(border, border), + QPointF(border, size - border), + QPointF(0, size) + }; + painter.setBrush(upLeftColor); + painter.drawPolygon(upLeftPolygon, 6); + painter.restore(); +} + +void paintColorSquareFrame(QPainter& painter, Variant variant, Color c, + qreal x, qreal y, qreal size, qreal alpha, + qreal saturation, bool flat) +{ + auto color = Util::getPaintColor(variant, c); + QColor upLeftColor; + QColor downRightColor; + if (flat) + { + upLeftColor = color; + downRightColor = color; + } + else + { + upLeftColor = color.lighter(130); + downRightColor = color.darker(160); + } + setAlphaSaturation(color, alpha, saturation); + setAlphaSaturation(upLeftColor, alpha, saturation); + setAlphaSaturation(downRightColor, alpha, saturation); + paintSquareFrame(painter, x, y, size, color, upLeftColor, downRightColor); +} + +} //namespace + +//----------------------------------------------------------------------------- + +string Util::convertSgfValueFromQString(const QString& value, + const string& charset) +{ + // Is there a way in Qt to support arbitrary Ascii-compatible text + // encodings? Currently, we only support UTF8 (used by Pentobi) and + // treat everything else as ISO-8859-1/Latin1 (the default for SGF) + // even if the charset property specifies some other encoding. + QString charsetToLower = QString(charset.c_str()).trimmed().toLower(); + if (charsetToLower == "utf-8" || charsetToLower == "utf8") + return value.toUtf8().constData(); + else + return value.toLatin1().constData(); +} + +QString Util::convertSgfValueToQString(const string& value, + const string& charset) +{ + // See comment in convertSgfValueFromQString() about supported encodings + QString charsetToLower = QString(charset.c_str()).trimmed().toLower(); + if (charsetToLower == "utf-8" || charsetToLower == "utf8") + return QString::fromUtf8(value.c_str()); + else + return QString::fromLatin1(value.c_str()); +} + +QColor Util::getLabelColor(Variant variant, PointState s) +{ + if (s.is_empty()) + return Qt::black; + Color c = s.to_color(); + QColor paintColor = getPaintColor(variant, c); + if (paintColor == yellow || paintColor == green) + return Qt::black; + else + return Qt::white; +} + +QColor Util::getMarkColor(Variant variant, PointState s) +{ + if (s.is_empty()) + return Qt::white; + Color c = s.to_color(); + QColor paintColor = getPaintColor(variant, c); + if (paintColor == yellow || paintColor == green) + return QColor("#333333"); + else + return Qt::white; +} + +QColor Util::getPaintColor(Variant variant, Color c) +{ + if (get_nu_colors(variant) == 2) + return c == Color(0) ? blue : green; + else + { + if (c == Color(0)) + return blue; + if (c == Color(1)) + return yellow; + if (c == Color(2)) + return red; + LIBBOARDGAME_ASSERT(c == Color(3)); + return green; + } +} + +QString Util::getPlayerString(Variant variant, Color c) +{ + auto i = c.to_int(); + if (get_nu_colors(variant) == 2) + return i == 0 ? qApp->translate("Util", "Blue") + : qApp->translate("Util", "Green"); + if (get_nu_players(variant) == 2) + return i == 0 || i == 2 ? qApp->translate("Util", "Blue/Red") + : qApp->translate("Util", "Yellow/Green"); + if (i == 0) + return qApp->translate("Util", "Blue"); + if (i == 1) + return qApp->translate("Util", "Yellow"); + if (i == 2) + return qApp->translate("Util", "Red"); + return qApp->translate("Util", "Green"); +} + +void Util::paintColorSegment(QPainter& painter, Variant variant, Color c, + bool isHorizontal, qreal x, qreal y, qreal size, + qreal alpha, qreal saturation, bool flat) +{ + auto color = getPaintColor(variant, c); + QColor upLeftColor; + QColor downRightColor; + if (flat) + { + upLeftColor = color; + downRightColor = color; + } + else + { + upLeftColor = color.lighter(130); + downRightColor = color.darker(160); + } + setAlphaSaturation(color, alpha, saturation); + setAlphaSaturation(upLeftColor, alpha, saturation); + setAlphaSaturation(downRightColor, alpha, saturation); + if (isHorizontal) + paintSquare(painter, x - size / 4, y + size / 4, 1.5 * size, size / 2, + color, upLeftColor, downRightColor); + else + paintSquare(painter, x + size / 4, y - size / 4, size / 2, 1.5 * size, + color, upLeftColor, downRightColor); +} + +void Util::paintColorSquare(QPainter& painter, Variant variant, Color c, + qreal x, qreal y, qreal size, qreal alpha, + qreal saturation, bool flat) +{ + auto color = getPaintColor(variant, c); + QColor upLeftColor; + QColor downRightColor; + if (flat) + { + upLeftColor = color; + downRightColor = color; + } + else + { + upLeftColor = color.lighter(130); + downRightColor = color.darker(160); + } + setAlphaSaturation(color, alpha, saturation); + setAlphaSaturation(upLeftColor, alpha, saturation); + setAlphaSaturation(downRightColor, alpha, saturation); + paintSquare(painter, x, y, size, size, color, upLeftColor, downRightColor); +} + +void Util::paintColorSquareCallisto(QPainter& painter, Variant variant, + Color c, qreal x, qreal y, qreal size, + bool hasRight, bool hasDown, + bool isOnePiece, qreal alpha, + qreal saturation, bool flat) +{ + auto color = getPaintColor(variant, c); + setAlphaSaturation(color, alpha, saturation); + if (hasRight) + painter.fillRect(QRectF(x + 0.96 * size, y + 0.07 * size, + 0.08 * size, 0.86 * size), color); + if (hasDown) + painter.fillRect(QRectF(x + 0.07 * size, y + 0.96 * size, + 0.86 * size, 0.08 * size), color); + if (isOnePiece) + paintColorSquareFrame(painter, variant, c, x + 0.04 * size, + y + 0.04 * size, 0.92 * size, alpha, saturation, + flat); + else + paintColorSquare(painter, variant, c, x + 0.04 * size, y + 0.04 * size, + 0.92 * size, alpha, saturation, flat); +} + +void Util::paintColorTriangle(QPainter& painter, Variant variant, + Color c, bool isUpward, qreal x, qreal y, + qreal width, qreal height, qreal alpha, + qreal saturation, bool flat) +{ + auto color = getPaintColor(variant, c); + QColor upLeftColor; + QColor downRightColor; + if (flat) + { + upLeftColor = color; + downRightColor = color; + } + else + { + upLeftColor = color.lighter(130); + downRightColor = color.darker(160); + } + setAlphaSaturation(color, alpha, saturation); + setAlphaSaturation(upLeftColor, alpha, saturation); + setAlphaSaturation(downRightColor, alpha, saturation); + paintTriangle(painter, isUpward, x, y, width, height, color, upLeftColor, + downRightColor); +} + +void Util::paintEmptyJunction(QPainter& painter, qreal x, qreal y, qreal size) +{ + painter.fillRect(QRectF(x + 0.25 * size, y + 0.25 * size, + 0.5 * size, 0.5 * size), + gray); +} + +void Util::paintEmptySegment(QPainter& painter, bool isHorizontal, qreal x, + qreal y, qreal size) +{ + if (isHorizontal) + paintSquare(painter, x - size / 4, y + size / 4, 1.5 * size, size / 2, + gray, gray.darker(130), gray.lighter(115)); + else + paintSquare(painter, x + size / 4, y - size / 4, size / 2, 1.5 * size, + gray, gray.darker(130), gray.lighter(115)); +} + +void Util::paintEmptySquare(QPainter& painter, qreal x, qreal y, qreal size) +{ + paintSquare(painter, x, y, size, size, gray, gray.darker(130), + gray.lighter(115)); +} + +void Util::paintEmptySquareCallisto(QPainter& painter, qreal x, qreal y, + qreal size) +{ + painter.fillRect(QRectF(x, y, size, size), gray); + paintSquare(painter, x + 0.04 * size, y + 0.04 * size, 0.92 * size, + 0.92 * size, gray, gray.darker(130), gray.lighter(115), true); +} + +void Util::paintEmptySquareCallistoCenter(QPainter& painter, qreal x, qreal y, + qreal size) +{ + painter.fillRect(QRectF(x, y, size, size), gray); + paintSquare(painter, x + 0.05 * size, y + 0.05 * size, 0.9 * size, + 0.9 * size, gray.darker(120), gray.darker(150), + gray.lighter(95), false); +} + +void Util::paintEmptyTriangle(QPainter& painter, bool isUpward, qreal x, + qreal y, qreal width, qreal height) +{ + paintTriangle(painter, isUpward, x, y, width, height, gray, + gray.darker(130), gray.lighter(115)); +} + +void Util::paintJunction(QPainter& painter, Variant variant, Color c, qreal x, + qreal y, qreal width, qreal height, bool hasLeft, + bool hasRight, bool hasUp, bool hasDown, qreal alpha, + qreal saturation) +{ + auto color = getPaintColor(variant, c); + setAlphaSaturation(color, alpha, saturation); + painter.save(); + painter.translate(x + 0.25 * width, y + 0.25 * height); + width *= 0.5; + height *= 0.5; + if (hasUp && hasDown) + painter.fillRect(QRectF(0.25 * width, 0, 0.5 * width, height), color); + if (hasLeft && hasRight) + painter.fillRect(QRectF(0, 0.25 * height, width, 0.5 * height), color); + painter.setPen(Qt::NoPen); + painter.setBrush(color); + if (hasLeft && hasUp) + { + const QPointF polygon[3] = { QPointF(0, 0), + QPointF(0.75 * width, 0), + QPointF(0, 0.75 * height) }; + painter.drawPolygon(polygon, 3); + } + if (hasRight && hasUp) + { + const QPointF polygon[3] = { QPointF(0.25 * width, 0), + QPointF(width, 0), + QPointF(width, 0.75 * height) }; + painter.drawPolygon(polygon, 3); + } + if (hasLeft && hasDown) + { + const QPointF polygon[3] = { QPointF(0, 0.25 * height), + QPointF(0, height), + QPointF(0.75 * width, height) }; + painter.drawPolygon(polygon, 3); + } + if (hasRight && hasDown) + { + const QPointF polygon[3] = { QPointF(0.25 * width, height), + QPointF(width, 0.25 * height), + QPointF(width, height) }; + painter.drawPolygon(polygon, 3); + } + painter.restore(); +} + +void Util::paintSegmentStartingPoint(QPainter& painter, Variant variant, + Color c, qreal x, qreal y, + qreal size) +{ + paintDot(painter, getPaintColor(variant, c), x, y, size, size, + 0.15 * size); +} + +void Util::paintSquareStartingPoint(QPainter& painter, Variant variant, + Color c, qreal x, qreal y, qreal size) +{ + paintDot(painter, getPaintColor(variant, c), x, y, size, size, + 0.13 * size); +} + +void Util::paintTriangleStartingPoint(QPainter& painter, bool isUpward, + qreal x, qreal y, qreal width, + qreal height) +{ + if (isUpward) + y += 0.333 * height; + height = 0.666 * height; + paintDot(painter, gray.darker(130), x, y, width, height, 0.17 * width); +} + +//----------------------------------------------------------------------------- diff --git a/src/libpentobi_gui/Util.h b/src/libpentobi_gui/Util.h new file mode 100644 index 0000000..4a070a3 --- /dev/null +++ b/src/libpentobi_gui/Util.h @@ -0,0 +1,112 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_gui/Util.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_GUI_UTIL_H +#define LIBPENTOBI_GUI_UTIL_H + +#include +#include +#include "libpentobi_base/Color.h" +#include "libpentobi_base/Variant.h" +#include "libpentobi_base/PointState.h" + +using namespace std; +using libpentobi_base::Color; +using libpentobi_base::Variant; +using libpentobi_base::PointState; + +//----------------------------------------------------------------------------- + +namespace Util +{ + +QColor getPaintColor(Variant variant, Color c); + +QColor getLabelColor(Variant variant, PointState s); + +QColor getMarkColor(Variant variant, PointState s); + +/** Paint piece segment in Nexos. */ +void paintColorSegment(QPainter& painter, Variant variant, Color c, + bool isHorizontal, qreal x, qreal y, qreal size, + qreal alpha = 1, qreal saturation = 1, + bool flat = false); + +void paintColorSquare(QPainter& painter, Variant variant, Color c, + qreal x, qreal y, qreal size, qreal alpha = 1, + qreal saturation = 1, bool flat = false); + +void paintColorSquareCallisto(QPainter& painter, Variant variant, Color c, + qreal x, qreal y, qreal size, bool hasRight, + bool hasDown, bool isOnePiece, qreal alpha = 1, + qreal saturation = 1, bool flat = false); + +void paintColorTriangle(QPainter& painter, Variant variant, + Color c, bool isUpward, qreal x, qreal y, qreal width, + qreal height, qreal alpha = 1, qreal saturation = 1, + bool flat = false); + +/** Paint empty junction in Nexos. */ +void paintEmptyJunction(QPainter& painter, qreal x, qreal y, qreal size); + +/** Paint empty segment in Nexos. */ +void paintEmptySegment(QPainter& painter, bool isHorizontal, qreal x, qreal y, + qreal size); + +void paintEmptySquare(QPainter& painter, qreal x, qreal y, qreal size); + +void paintEmptySquareCallisto(QPainter& painter, qreal x, qreal y, qreal size); + +void paintEmptySquareCallistoCenter(QPainter& painter, qreal x, qreal y, + qreal size); + +void paintEmptyTriangle(QPainter& painter, bool isUpward, qreal x, qreal y, + qreal width, qreal height); + +void paintJunction(QPainter& painter, Variant variant, Color c, qreal x, + qreal y, qreal width, qreal height, bool hasLeft, + bool hasRight, bool hasUp, bool hasDown, qreal alpha = 1, + qreal saturation = 1); + +/** Paint starting point in Nexos. */ +void paintSegmentStartingPoint(QPainter& painter, Variant variant, Color c, + qreal x, qreal y, qreal size); + +void paintSquareStartingPoint(QPainter& painter, Variant variant, Color c, + qreal x, qreal y, qreal size); + +void paintTriangleStartingPoint(QPainter& painter, bool isUpward, qreal x, + qreal y, qreal width, qreal height); + +/** Convert a property value of a SGF tree unto a QString. + @param value + @param charset The value of the CA property of the root node in the tree + or an empty string if the tree has no such property. + This function currently only recognizes UTF8 and ISO-8859-1 (the latter + is the default for SGF if no CA property exists). Other charsets are + ignored and the string is converted using the default system charset. */ +string convertSgfValueFromQString(const QString& value, const string& charset); + +/** Convert a property value of a SGF tree unto a QString. + @param value + @param charset The value of the CA property of the root node in the tree + or an empty string if the tree has no such property. + This function currently only recognizes UTF8 and ISO-8859-1 (the latter + is the default for SGF if no CA property exists). Other charsets are + ignored and the string is converted using the default system charset. */ +QString convertSgfValueToQString(const string& value, const string& charset); + +/** Get a translated string identifying a player, like "Blue" or "Blue/Red". + @param variant The game variant + @param c The player color or one of the player colors in game variants + with multiple colors per player. */ +QString getPlayerString(Variant variant, Color c); + +} + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_GUI_UTIL_H diff --git a/src/libpentobi_gui/icons/go-home.svg b/src/libpentobi_gui/icons/go-home.svg new file mode 100644 index 0000000..61e1d58 --- /dev/null +++ b/src/libpentobi_gui/icons/go-home.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/libpentobi_gui/icons/go-next.svg b/src/libpentobi_gui/icons/go-next.svg new file mode 100644 index 0000000..0d797a9 --- /dev/null +++ b/src/libpentobi_gui/icons/go-next.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/libpentobi_gui/icons/go-previous.svg b/src/libpentobi_gui/icons/go-previous.svg new file mode 100644 index 0000000..0aee1b4 --- /dev/null +++ b/src/libpentobi_gui/icons/go-previous.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/libpentobi_gui/libpentobi_gui_resources.qrc b/src/libpentobi_gui/libpentobi_gui_resources.qrc new file mode 100644 index 0000000..0206df4 --- /dev/null +++ b/src/libpentobi_gui/libpentobi_gui_resources.qrc @@ -0,0 +1,8 @@ + + + + icons/go-home.png + icons/go-next.png + icons/go-previous.png + + diff --git a/src/libpentobi_gui/libpentobi_gui_resources_2x.qrc b/src/libpentobi_gui/libpentobi_gui_resources_2x.qrc new file mode 100644 index 0000000..e81e663 --- /dev/null +++ b/src/libpentobi_gui/libpentobi_gui_resources_2x.qrc @@ -0,0 +1,8 @@ + + + + icons/go-home@2x.png + icons/go-next@2x.png + icons/go-previous@2x.png + + diff --git a/src/libpentobi_gui/translations/libpentobi_gui_de.ts b/src/libpentobi_gui/translations/libpentobi_gui_de.ts new file mode 100644 index 0000000..cb3ef0b --- /dev/null +++ b/src/libpentobi_gui/translations/libpentobi_gui_de.ts @@ -0,0 +1,170 @@ + + + + + ComputerColorDialog + + Computer Colors + Computer-Farben + + + Computer plays: + Computer spielt: + + + &Blue + &Blau + + + &Green + &Grün + + + &Yellow + G&elb + + + &Red + &Rot + + + &Blue/Red + &Blau/Rot + + + &Yellow/Green + &Gelb/Grün + + + + GameInfoDialog + + Game Info + Spielinformation + + + Player &Blue: + Player Blue: + Spieler &Blau: + + + Player &Green: + Player Green: + Spieler &Grün: + + + Player &Yellow: + Player Yellow: + Spieler G&elb: + + + Player &Red: + Player Red: + Spieler &Rot: + + + Player &Blue/Red: + Player Blue/Red: + Spieler &Blau/Rot: + + + Player &Yellow/Green: + Player Yellow/Green: + Spieler &Gelb/Grün: + + + &Date: + Date: + &Datum: + + + &Time limits: + Bedenk&zeit: + + + &Event: + &Veranstaltung: + + + R&ound: + R&unde: + + + + HelpWindow + + Back + Zurück + + + Show previous page in history + Die vorherige Seite in der Chronik anzeigen + + + Forward + Vorwärts + + + Show next page in history + Die nächste Seite in der Chronik anzeigen + + + Contents + Inhalt + + + Show table of contents + Das Inhaltsverzeichnis anzeigen + + + + InitialRatingDialog + + Initial Rating + Anfangswertung + + + You have not yet played rated games in this game variant. Estimate your playing strength to initialize your rating. + Sie haben noch keine gewerteten Spiele in dieser Spielvariante gespielt. Schätzen Sie Ihre Spielstärke, um Ihre Wertung zu initialisieren. + + + Beginner + Anfänger + + + Expert + Experte + + + Your initial rating: %1 + Ihre Anfangswertung: %1 + + + + Util + + Blue + Blau + + + Green + Grün + + + Yellow + Gelb + + + Red + Rot + + + Blue/Red + Blau/Rot + + + Yellow/Green + Gelb/Grün + + + diff --git a/src/libpentobi_kde_thumbnailer/CMakeLists.txt b/src/libpentobi_kde_thumbnailer/CMakeLists.txt new file mode 100644 index 0000000..a721e3d --- /dev/null +++ b/src/libpentobi_kde_thumbnailer/CMakeLists.txt @@ -0,0 +1,38 @@ +# libpentobi_kde_thumbnailer contains the files needed by +# the pentobi_kde_thumbnailer plugin compiled with shared library options +# (usually -fPIC) because this is required for building shared libraries on +# some targets (e.g. x86_64). +# +# The alternative would be to add -fPIC to the global compiler flags even for +# executables but this slows down Pentobi's search by 10% on some targets. +# +# Adding the source files directly to pentobi_kde_thumbnailer/CMakeList.txt is +# not possible because the KDE CMake macros add -fno-exceptions to the +# compiler flags, which causes errors in the Pentobi sources that use +# exceptions (which should be fine as long as no exceptions are thrown +# from the thumbnailer plugin functions). + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${CMAKE_SHARED_LIBRARY_CXX_FLAGS}") + +add_library(pentobi_kde_thumbnailer STATIC + ../libboardgame_util/Assert.cpp + ../libboardgame_util/Log.cpp + ../libboardgame_util/StringUtil.cpp + ../libboardgame_base/StringRep.cpp + ../libboardgame_sgf/MissingProperty.cpp + ../libboardgame_sgf/Reader.cpp + ../libboardgame_sgf/SgfNode.cpp + ../libboardgame_sgf/SgfTree.cpp + ../libboardgame_sgf/TreeReader.cpp + ../libpentobi_base/CallistoGeometry.cpp + ../libpentobi_base/NexosGeometry.cpp + ../libpentobi_base/NodeUtil.cpp + ../libpentobi_base/StartingPoints.cpp + ../libpentobi_base/TrigonGeometry.cpp + ../libpentobi_base/Variant.cpp + ../libpentobi_gui/BoardPainter.cpp + ../libpentobi_gui/Util.cpp + ../libpentobi_thumbnail/CreateThumbnail.cpp +) + +target_link_libraries(pentobi_kde_thumbnailer Qt5::Widgets) diff --git a/src/libpentobi_mcts/AnalyzeGame.cpp b/src/libpentobi_mcts/AnalyzeGame.cpp new file mode 100644 index 0000000..f4d68b9 --- /dev/null +++ b/src/libpentobi_mcts/AnalyzeGame.cpp @@ -0,0 +1,108 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/AnalyzeGame.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "AnalyzeGame.h" + +#include "Search.h" +#include "libboardgame_util/Log.h" +#include "libboardgame_util/WallTimeSource.h" + +namespace libpentobi_mcts { + +using libboardgame_sgf::InvalidTree; +using libboardgame_sgf::SgfNode; +using libboardgame_util::clear_abort; +using libboardgame_util::get_abort; +using libboardgame_util::WallTimeSource; +using libpentobi_base::BoardUpdater; + +//----------------------------------------------------------------------------- + +void AnalyzeGame::run(const Game& game, Search& search, size_t nu_simulations, + function progress_callback) +{ + m_variant = game.get_variant(); + m_moves.clear(); + m_values.clear(); + auto& tree = game.get_tree(); + unique_ptr bd(new Board(m_variant)); + BoardUpdater updater; + auto& root = game.get_root(); + auto node = &root; + unsigned total_moves = 0; + try { + while (node) + { + if (tree.has_move(*node)) + ++total_moves; + node = node->get_first_child_or_null(); + } + } + catch (const InvalidTree&) + { + // PentobiTree::has_move() can throw on invalid SGF tree read from + // external file. We simply abort the analysis. + return; + } + WallTimeSource time_source; + clear_abort(); + node = &root; + unsigned move_number = 0; + auto tie_value = Search::SearchParamConst::tie_value; + while (node) + { + auto mv = tree.get_move(*node); + if (! mv.is_null()) + { + if (! node->has_parent()) + { + // Root shouldn't contain moves in SGF files + m_moves.push_back(mv); + m_values.push_back(tie_value); + } + else + { + progress_callback(move_number, total_moves); + try + { + updater.update(*bd, tree, node->get_parent()); + LIBBOARDGAME_LOG("Analyzing move ", bd->get_nu_moves()); + const Float max_count = Float(nu_simulations); + double max_time = 0; + // Set min_simulations to a reasonable value because + // nu_simulations can be reached without having that many + // value updates if a subtree from a previous search is + // reused (which re-initializes the value and value count + // of the new root from the best child) + size_t min_simulations = min(size_t(100), nu_simulations); + Move computer_mv; + search.search(computer_mv, *bd, mv.color, max_count, + min_simulations, max_time, time_source); + if (get_abort()) + break; + m_moves.push_back(mv); + m_values.push_back(search.get_root_val().get_mean()); + } + catch (const InvalidTree&) + { + // BoardUpdater::update() can throw on invalid SGF tree + // read from external file. We simply abort the analysis. + break; + } + } + ++move_number; + } + node = node->get_first_child_or_null(); + } +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts diff --git a/src/libpentobi_mcts/AnalyzeGame.h b/src/libpentobi_mcts/AnalyzeGame.h new file mode 100644 index 0000000..3b7e601 --- /dev/null +++ b/src/libpentobi_mcts/AnalyzeGame.h @@ -0,0 +1,83 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/AnalyzeGame.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_MCTS_ANALYZE_GAME_H +#define LIBPENTOBI_MCTS_ANALYZE_GAME_H + +#include +#include +#include "libpentobi_base/Game.h" + +namespace libpentobi_mcts { + +class Search; + +using namespace std; +using libpentobi_base::ColorMove; +using libpentobi_base::Game; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +/** Evaluate each position in the main variation of a game. */ +class AnalyzeGame +{ +public: + /** Run the analysis. + The analysis can be aborted from a different thread with + libboardgame_util::set_abort(). + @param game + @param search + @param nu_simulations + @param progress_callback Function that will be called at the beginning + of the analysis of a position. Arguments: number moves analyzed so far, + total number of moves. */ + void run(const Game& game, Search& search, size_t nu_simulations, + function progress_callback); + + Variant get_variant() const; + + unsigned get_nu_moves() const; + + ColorMove get_move(unsigned i) const; + + double get_value(unsigned i) const; + +private: + Variant m_variant; + + vector m_moves; + + vector m_values; +}; + +inline ColorMove AnalyzeGame::get_move(unsigned i) const +{ + LIBBOARDGAME_ASSERT(i < m_moves.size()); + return m_moves[i]; +} + +inline unsigned AnalyzeGame::get_nu_moves() const +{ + return static_cast(m_moves.size()); +} + +inline double AnalyzeGame::get_value(unsigned i) const +{ + LIBBOARDGAME_ASSERT(i < m_values.size()); + return m_values[i]; +} + +inline Variant AnalyzeGame::get_variant() const +{ + return m_variant; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts + +#endif // LIBPENTOBI_MCTS_ANALYZE_GAME_H diff --git a/src/libpentobi_mcts/CMakeLists.txt b/src/libpentobi_mcts/CMakeLists.txt new file mode 100644 index 0000000..9acef96 --- /dev/null +++ b/src/libpentobi_mcts/CMakeLists.txt @@ -0,0 +1,24 @@ +add_library(pentobi_mcts STATIC + AnalyzeGame.h + AnalyzeGame.cpp + Float.h + History.h + History.cpp + Player.h + Player.cpp + PlayoutFeatures.h + PlayoutFeatures.cpp + PriorKnowledge.h + PriorKnowledge.cpp + SearchParamConst.h + SharedConst.h + SharedConst.cpp + Search.h + Search.cpp + State.h + State.cpp + StateUtil.h + StateUtil.cpp + Util.h + Util.cpp +) diff --git a/src/libpentobi_mcts/Float.h b/src/libpentobi_mcts/Float.h new file mode 100644 index 0000000..10ece26 --- /dev/null +++ b/src/libpentobi_mcts/Float.h @@ -0,0 +1,28 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/Float.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_MCTS_FLOAT_H +#define LIBPENTOBI_MCTS_FLOAT_H + +#include + +namespace libpentobi_mcts { + +//----------------------------------------------------------------------------- + +#ifdef LIBPENTOBI_MCTS_FLOAT_TYPE +typedef LIBPENTOBI_MCTS_FLOAT_TYPE Float; +#else +typedef float Float; +#endif + +static_assert(std::is_floating_point::value, ""); + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts + +#endif // LIBPENTOBI_MCTS_FLOAT_H diff --git a/src/libpentobi_mcts/History.cpp b/src/libpentobi_mcts/History.cpp new file mode 100644 index 0000000..290df35 --- /dev/null +++ b/src/libpentobi_mcts/History.cpp @@ -0,0 +1,77 @@ +//---------------------------------------------------------------------------- +/** @file libpentobi_mcts/History.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//---------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "History.h" + +#include "libpentobi_base/BoardUtil.h" + +namespace libpentobi_mcts { + +using namespace std; +using libpentobi_base::boardutil::get_current_position_as_setup; + +//---------------------------------------------------------------------------- + +void History::get_as_setup(Variant& variant, Setup& setup) const +{ + LIBBOARDGAME_ASSERT(is_valid()); + variant = m_variant; + unique_ptr bd(new Board(variant)); + for (ColorMove mv : m_moves) + bd->play(mv); + get_current_position_as_setup(*bd, setup); +} + +void History::init(const Board& bd, Color to_play) +{ + m_is_valid = true; + m_variant = bd.get_variant(); + m_nu_colors = bd.get_nu_colors(); + m_moves.clear(); + for (unsigned i = 0; i < bd.get_nu_moves(); ++i) + m_moves.push_back(bd.get_move(i)); + m_to_play = to_play; +} + +bool History::is_followup( + const History& other, + ArrayList& sequence) const +{ + if (! m_is_valid || ! other.m_is_valid || m_variant != other.m_variant + || m_moves.size() < other.m_moves.size()) + return false; + unsigned i = 0; + for ( ; i < other.m_moves.size(); ++i) + if (m_moves[i] != other.m_moves[i]) + return false; + sequence.clear(); + Color to_play = other.m_to_play; + for ( ; i < m_moves.size(); ++i) + { + auto mv = m_moves[i]; + while (mv.color != to_play) + { + sequence.push_back(Move::null()); + to_play = to_play.get_next(m_nu_colors); + } + sequence.push_back(mv.move); + to_play = to_play.get_next(m_nu_colors); + } + while (m_to_play != to_play) + { + sequence.push_back(Move::null()); + to_play = to_play.get_next(m_nu_colors); + } + return true; +} + +//---------------------------------------------------------------------------- + +} // namespace libpentobi_mcts diff --git a/src/libpentobi_mcts/History.h b/src/libpentobi_mcts/History.h new file mode 100644 index 0000000..8d9f145 --- /dev/null +++ b/src/libpentobi_mcts/History.h @@ -0,0 +1,105 @@ +//---------------------------------------------------------------------------- +/** @file libpentobi_mcts/History.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//---------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_MCTS_HISTORY_H +#define LIBPENTOBI_MCTS_HISTORY_H + +#include "SearchParamConst.h" +#include "libpentobi_base/Board.h" + +namespace libpentobi_mcts { + +using libboardgame_util::ArrayList; +using libpentobi_base::Board; +using libpentobi_base::Color; +using libpentobi_base::ColorMove; +using libpentobi_base::Move; +using libpentobi_base::Setup; +using libpentobi_base::Variant; + +//---------------------------------------------------------------------------- + +/** Identifier for board state including history. + This class can be used, for instance, to uniquely remember a board + position for reusing parts of previous computations. The state includes: + - the game variant + - the history of moves + - the color to play */ +class History +{ +public: + /** Constructor. + The initial state is that the history does not correspond to any + valid position. */ + History(); + + /** Initialize from a current board position and explicit color to play. */ + void init(const Board& bd, Color to_play); + + /** Clear the state. + A cleared state does not correspond to any valid position. */ + void clear(); + + /** Check if the state corresponds to any valid position. */ + bool is_valid() const; + + /** Check if this position is a alternate-play followup to another one. + @param other The other position + @param[out] sequence The sequence leading from the other position to + this one. Pass (=null) moves are inserted to ensure alternating colors + (as required by libpentobi_mcts::Search.) + @return @c true If the position is a followup + */ + bool is_followup( + const History& other, + ArrayList& sequence) const; + + /** Get the position of the board state as setup. + @pre is_valid() + @param[out] variant + @param[out] setup */ + void get_as_setup(Variant& variant, Setup& setup) const; + + Color get_to_play() const; + +private: + bool m_is_valid; + + Color::IntType m_nu_colors; + + Variant m_variant; + + ArrayList m_moves; + + Color m_to_play; +}; + +inline History::History() +{ + clear(); +} + +inline void History::clear() +{ + m_is_valid = false; +} + +inline Color History::get_to_play() const +{ + LIBBOARDGAME_ASSERT(m_is_valid); + return m_to_play; +} + +inline bool History::is_valid() const +{ + return m_is_valid; +} + +//---------------------------------------------------------------------------- + +} // namespace libpentobi_mcts + +#endif // LIBPENTOBI_MCTS_HISTORY_H diff --git a/src/libpentobi_mcts/Player.cpp b/src/libpentobi_mcts/Player.cpp new file mode 100644 index 0000000..5bd0a69 --- /dev/null +++ b/src/libpentobi_mcts/Player.cpp @@ -0,0 +1,366 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/Player.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Player.h" + +#include +#include +#include "libboardgame_util/CpuTimeSource.h" +#include "libboardgame_util/WallTimeSource.h" +#include "libboardgame_sys/Memory.h" + +namespace libpentobi_mcts { + +using libboardgame_util::CpuTimeSource; +using libboardgame_util::WallTimeSource; +using libpentobi_base::BoardType; + +//----------------------------------------------------------------------------- + +namespace { + +// Rationale for choosing the number of simulations: +// * Level 9, the highest in the desktop version, should be as strong as +// possible on a mid-range PC with reasonable thinking times. The average +// time per game and player is targeted at 2-3 min for the 2-color game +// variants and 5-6 min for the others. +// * Level 7, the highest in the Android version, should be as strong as +// possible on typical mobile hardware. It is set to 4% of level 9. +// * Level 8 is set to 20% of level 9, the middle (on a log scale) between +// level 7 and 9. Since most parameter tuning is done at level 7 or 8, it is +// better for development purposes to define level 8 in terms of time, even +// if it doesn't necessarily correspond to the middle wrt. playing strength. +// * The numbers for level 1 are set to a value that is weak enough for +// beginners without playing silly moves. They are currently chosen depending +// on how strong we estimate Pentobi is in a game variant. It is also taken +// into consideration how much the Elo difference level 1-9 is in self-play +// experiments. After applying the scale factor (see comment in +// Player::get_rating()), we want a range of about 1000 Elo (difference +// between beginner and lower master level). +// * The numbers for level 1-6 are chosen such that they correspond to roughly +// equidistant Elo differences measured in self-play experiments. + +static const float counts_classic[Player::max_supported_level] = + { 3, 18, 75, 311, 1260, 8949, 66179, 330894, 1654470 }; + +static const float counts_duo[Player::max_supported_level] = + { 3, 14, 63, 253, 2203, 13614, 202994, 1014969, 5074843 }; + +static const float counts_trigon[Player::max_supported_level] = + { 228, 376, 733, 1214, 2606, 6802, 18428, 92138, 460691 }; + +static const float counts_nexos[Player::max_supported_level] = + { 250, 347, 625, 1223, 3117, 8270, 22626, 113130, 565651 }; + +static const float counts_callisto_2[Player::max_supported_level] = + { 100, 192, 405, 1079, 3323, 12258, 94104, 470522, 2352609 }; + +} // namespace + +//----------------------------------------------------------------------------- + +Player::Player(Variant initial_variant, unsigned max_level, string books_dir, + unsigned nu_threads) + : m_is_book_loaded(false), + m_use_book(true), + m_resign(false), + m_books_dir(move(books_dir)), + m_max_level(max_level), + m_level(4), + m_fixed_simulations(0), + m_resign_threshold(0.09f), + m_resign_min_simulations(500), + m_search(initial_variant, nu_threads, get_memory()), + m_book(initial_variant), + m_time_source(new WallTimeSource) +{ + for (unsigned i = 0; i < Board::max_player_moves; ++i) + { + // Hand-tuned such that time per move is more evenly spread among all + // moves than with a fixed number of simulations (because the + // simulations per second increase rapidly with the move number) but + // the average time per game is roughly the same. + m_weight_max_count_duo[i] = 0.7f * exp(0.1f * static_cast(i)); + m_weight_max_count_classic[i] = m_weight_max_count_duo[i]; + m_weight_max_count_trigon[i] = m_weight_max_count_duo[i]; + m_weight_max_count_callisto[i] = m_weight_max_count_duo[i]; + m_weight_max_count_callisto_2[i] = m_weight_max_count_duo[i]; + // Less weight for the first move(s) because number of legal moves + // is lower and the search applies some pruning rules to reduce the + // branching factor in early moves + if (i == 0) + { + m_weight_max_count_classic[i] *= 0.2f; + m_weight_max_count_trigon[i] *= 0.2f; + m_weight_max_count_duo[i] *= 0.6f; + m_weight_max_count_callisto[i] *= 0.2f; + m_weight_max_count_callisto_2[i] *= 0.2f; + } + else if (i == 1) + { + m_weight_max_count_classic[i] *= 0.2f; + m_weight_max_count_trigon[i] *= 0.5f; + m_weight_max_count_callisto[i] *= 0.6f; + m_weight_max_count_callisto_2[i] *= 0.2f; + } + else if (i == 2) + { + m_weight_max_count_classic[i] *= 0.3f; + m_weight_max_count_trigon[i] *= 0.6f; + } + else if (i == 3) + { + m_weight_max_count_trigon[i] *= 0.8f; + } + } +} + +Player::~Player() = default; + +Move Player::genmove(const Board& bd, Color c) +{ + m_resign = false; + if (! bd.has_moves(c)) + return Move::null(); + Move mv; + auto variant = bd.get_variant(); + auto board_type = bd.get_board_type(); + auto level = min(max(m_level, 1u), m_max_level); + // Don't use more than 2 moves per color from opening book in lower levels + if (m_use_book + && (level >= 4 || bd.get_nu_moves() < 2u * bd.get_nu_colors())) + { + if (! is_book_loaded(variant)) + load_book(m_books_dir + + "/book_" + to_string_id(variant) + ".blksgf"); + if (m_is_book_loaded) + { + mv = m_book.genmove(bd, c); + if (! mv.is_null()) + return mv; + } + } + Float max_count = 0; + double max_time = 0; + if (m_fixed_simulations > 0) + max_count = m_fixed_simulations; + else if (m_fixed_time > 0) + max_time = m_fixed_time; + else + { + switch (board_type) + { + case BoardType::classic: + max_count = counts_classic[level - 1]; + break; + case BoardType::duo: + max_count = counts_duo[level - 1]; + break; + case BoardType::trigon: + case BoardType::trigon_3: + case BoardType::callisto: + case BoardType::callisto_3: + max_count = counts_trigon[level - 1]; + break; + case BoardType::nexos: + max_count = counts_nexos[level - 1]; + break; + case BoardType::callisto_2: + max_count = counts_callisto_2[level - 1]; + break; + } + // Don't weight max_count in low levels, otherwise it is still too + // strong for beginners (later in the game, the weight becomes much + // greater than 1 because the simulations become very fast) + bool weight_max_count = (level >= 4); + if (weight_max_count) + { + auto player_move = bd.get_nu_onboard_pieces(c); + float weight = 1; // Init to avoid compiler warning + switch (board_type) + { + case BoardType::classic: + weight = m_weight_max_count_classic[player_move]; + break; + case BoardType::duo: + weight = m_weight_max_count_duo[player_move]; + break; + case BoardType::callisto: + case BoardType::callisto_3: + weight = m_weight_max_count_callisto[player_move]; + break; + case BoardType::callisto_2: + weight = m_weight_max_count_callisto_2[player_move]; + break; + case BoardType::trigon: + case BoardType::trigon_3: + case BoardType::nexos: + weight = m_weight_max_count_trigon[player_move]; + break; + } + max_count = ceil(max_count * weight); + } + } + if (max_count != 0) + LIBBOARDGAME_LOG("MaxCnt ", fixed, setprecision(0), max_count); + else + LIBBOARDGAME_LOG("MaxTime ", max_time); + if (! m_search.search(mv, bd, c, max_count, 0, max_time, *m_time_source)) + return Move::null(); + // Resign only in two-player game variants + if (get_nu_players(variant) == 2) + if (m_search.get_root_visit_count() > m_resign_min_simulations + && m_search.get_root_val().get_mean() < m_resign_threshold) + m_resign = true; + return mv; +} + +/** Suggest how much memory to use for the trees depending on the maximum + level used. */ +size_t Player::get_memory() +{ + size_t available = libboardgame_sys::get_memory(); + if (available == 0) + { + LIBBOARDGAME_LOG("WARNING: could not determine system memory" + " (assuming 512MB)"); + available = 512000000; + } + // Don't use all of the available memory +#if PENTOBI_LOW_RESOURCES + size_t reasonable = available / 4; +#else + size_t reasonable = available / 3; +#endif + size_t wanted = 2000000000; + if (m_max_level < max_supported_level) + { + // We don't need so much memory if m_max_level is smaller than + // max_supported_level. Trigon has the highest relative number of + // simulations on lower levels compared to the highest level. The + // memory used in a search is not proportional to the number of + // simulations (e.g. because the expand threshold increases with the + // depth). We approximate this by adding an exponent to the ratio + // and not taking into account if m_max_level is very small. + LIBBOARDGAME_ASSERT(max_supported_level >= 5); + auto factor = pow(counts_trigon[max_supported_level - 1] + / counts_trigon[max(m_max_level, 5u) - 1], 0.8); + wanted = static_cast(double(wanted) / factor); + } + size_t memory = min(wanted, reasonable); + LIBBOARDGAME_LOG("Using ", memory / 1000000, " MB of ", + available / 1000000, " MB"); + return memory; +} + +Rating Player::get_rating(Variant variant, unsigned level) +{ + // The ratings are roughly based on Elo differences measured in self-play + // experiments. The measured values are scaled with a factor smaller than 1 + // to take into account that self-play usually overestimates the strength + // against humans. The anchor is set to about 1000 (beginner level) for + // level 1. The exact value for anchor and scale is chosen according to our + // estimate how strong Pentobi plays at level 1 and level 9 in each game + // variant (2000 Elo would be lower expert level). Currently, only 2-player + // variants are tested and the ratings are used for multi-player variants + // on the same board. + auto max_supported_level = Player::max_supported_level; + level = min(max(level, 1u), max_supported_level); + Rating result; + switch (get_board_type(variant)) + { + case BoardType::classic: + { + // Anchor 1000, scale 0.63 + static float elo[Player::max_supported_level] = + { 1000, 1145, 1290, 1435, 1580, 1725, 1870, 1957, 2021 }; + result = Rating(elo[level - 1]); + } + break; + case BoardType::duo: + { + // Anchor 1100, scale 0.7 + static float elo[Player::max_supported_level] = + { 1100, 1269, 1438, 1607, 1776, 1945, 2114, 2165, 2209 }; + result = Rating(elo[level - 1]); + } + break; + case BoardType::callisto_2: + { + // Anchor 1000, scale 0.63 + static float elo[Player::max_supported_level] = + { 1000, 1101, 1203, 1304, 1405, 1507, 1608, 1673, 1756 }; + result = Rating(elo[level - 1]); + } + break; + case BoardType::trigon: + case BoardType::trigon_3: + { + // Anchor 1000, scale 0.60 + static float elo[Player::max_supported_level] = + { 1000, 1103, 1206, 1308, 1411, 1514, 1617, 1757, 1856 }; + result = Rating(elo[level - 1]); + } + break; + case BoardType::nexos: + case BoardType::callisto: // Not measured + case BoardType::callisto_3: // Not measured + { + // Anchor 1000, scale 0.60 + static float elo[Player::max_supported_level] = + { 1000, 1101, 1202, 1304, 1406, 1507, 1608, 1698, 1799 }; + result = Rating(elo[level - 1]); + } + break; + } + return result; +} + +bool Player::is_book_loaded(Variant variant) const +{ + return m_is_book_loaded && m_book.get_tree().get_variant() == variant; +} + +void Player::load_book(istream& in) +{ + m_book.load(in); + m_is_book_loaded = true; +} + +bool Player::load_book(const string& filepath) +{ + ifstream in(filepath); + if (! in) + { + LIBBOARDGAME_LOG("Could not load book ", filepath); + return false; + } + m_book.load(in); + m_is_book_loaded = true; + LIBBOARDGAME_LOG("Loaded book ", filepath); + return true; +} + +bool Player::resign() const +{ + return m_resign; +} + +void Player::use_cpu_time(bool enable) +{ + if (enable) + m_time_source.reset(new CpuTimeSource); + else + m_time_source.reset(new WallTimeSource); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts diff --git a/src/libpentobi_mcts/Player.h b/src/libpentobi_mcts/Player.h new file mode 100644 index 0000000..3aeb8ce --- /dev/null +++ b/src/libpentobi_mcts/Player.h @@ -0,0 +1,193 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/Player.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_MCTS_PLAYER_H +#define LIBPENTOBI_MCTS_PLAYER_H + +#include "Search.h" +#include "libboardgame_base/Rating.h" +#include "libpentobi_base/Book.h" +#include "libpentobi_base/PlayerBase.h" + +namespace libpentobi_mcts { + +using libboardgame_base::Rating; +using libpentobi_base::Book; +using libpentobi_base::PlayerBase; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +class Player final + : public PlayerBase +{ +public: + static const unsigned max_supported_level = 9; + + /** Constructor. + @param initial_variant Game variant to initialize the internal + board with (may avoid unnecessary BoardConst creation for game variant + that is never used) + @param max_level The maximum level used + @param books_dir Directory containing opening books. + @param nu_threads The number of threads to use in the search (0 means + to select a reasonable default value) */ + Player(Variant initial_variant, unsigned max_level, string books_dir, unsigned nu_threads = 0); + + ~Player(); + + Move genmove(const Board& bd, Color c) override; + + bool resign() const override; + + Float get_fixed_simulations() const; + + double get_fixed_time() const; + + /** Use a fixed number of simulations in the search. + If set to a value greater than zero, this value will enforce a + fixed number of simulations per search independent of the playing + level. */ + void set_fixed_simulations(Float n); + + /** Use a fixed time limit per move. + If set to a value greater than zero, this value will set a fixed + (maximum) time per search independent of the playing level. */ + void set_fixed_time(double seconds); + + bool get_use_book() const; + + void set_use_book(bool enable); + + unsigned get_level() const; + + void set_level(unsigned level); + + /** Use CPU time instead of Wall time to measure time. */ + void use_cpu_time(bool enable); + + Search& get_search(); + + void load_book(istream& in); + + /** Is a book loaded and compatible with a given game variant? */ + bool is_book_loaded(Variant variant) const; + + /** Get an estimated Elo-rating of a level. + This rating is an estimated rating when playing vs. humans. Although + it is based on computer vs. computer experiments, the ratings were + modified and rescaled to take into account that self-play experiments + usually overestimate the rating differences when playing against + humans. */ + static Rating get_rating(Variant variant, unsigned level); + + /** Get an estimated Elo-rating of the current level. */ + Rating get_rating(Variant variant) const; + +private: + bool m_is_book_loaded; + + bool m_use_book; + + bool m_resign; + + string m_books_dir; + + unsigned m_max_level; + + unsigned m_level; + + array m_weight_max_count_classic; + + array m_weight_max_count_trigon; + + array m_weight_max_count_duo; + + array m_weight_max_count_callisto; + + array m_weight_max_count_callisto_2; + + Float m_fixed_simulations; + + Float m_resign_threshold; + + Float m_resign_min_simulations; + + double m_fixed_time; + + Search m_search; + + Book m_book; + + unique_ptr m_time_source; + + + size_t get_memory(); + + void init_settings(); + + bool load_book(const string& filepath); +}; + +inline Float Player::get_fixed_simulations() const +{ + return m_fixed_simulations; +} + +inline double Player::get_fixed_time() const +{ + return m_fixed_time; +} + +inline unsigned Player::get_level() const +{ + return m_level; +} + +inline Rating Player::get_rating(Variant variant) const +{ + return get_rating(variant, m_level); +} + +inline Search& Player::get_search() +{ + return m_search; +} + +inline bool Player::get_use_book() const +{ + return m_use_book; +} + +inline void Player::set_fixed_simulations(Float n) +{ + m_fixed_simulations = n; + m_fixed_time = 0; +} + +inline void Player::set_fixed_time(double seconds) +{ + m_fixed_time = seconds; + m_fixed_simulations = 0; +} + +inline void Player::set_level(unsigned level) +{ + m_level = level; + m_fixed_simulations = 0; + m_fixed_time = 0; +} + +inline void Player::set_use_book(bool enable) +{ + m_use_book = enable; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts + +#endif // LIBPENTOBI_MCTS_PLAYER_H diff --git a/src/libpentobi_mcts/PlayoutFeatures.cpp b/src/libpentobi_mcts/PlayoutFeatures.cpp new file mode 100644 index 0000000..54fe564 --- /dev/null +++ b/src/libpentobi_mcts/PlayoutFeatures.cpp @@ -0,0 +1,21 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/PlayoutFeatures.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "PlayoutFeatures.h" + +namespace libpentobi_mcts { + +//----------------------------------------------------------------------------- + +PlayoutFeatures::PlayoutFeatures() = default; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts diff --git a/src/libpentobi_mcts/PlayoutFeatures.h b/src/libpentobi_mcts/PlayoutFeatures.h new file mode 100644 index 0000000..a3a535f --- /dev/null +++ b/src/libpentobi_mcts/PlayoutFeatures.h @@ -0,0 +1,215 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/PlayoutFeatures.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_MCTS_PLAYOUT_FEATURES_H +#define LIBPENTOBI_MCTS_PLAYOUT_FEATURES_H + +#include "libpentobi_base/Board.h" +#include "libpentobi_base/PointList.h" + +namespace libpentobi_mcts { + +using namespace std; +using libboardgame_base::ArrayList; +using libboardgame_util::Range; +using libpentobi_base::Board; +using libpentobi_base::BoardConst; +using libpentobi_base::Color; +using libpentobi_base::ColorMove; +using libpentobi_base::Geometry; +using libpentobi_base::Grid; +using libpentobi_base::GridExt; +using libpentobi_base::Move; +using libpentobi_base::MoveInfo; +using libpentobi_base::MoveInfoExt; +using libpentobi_base::PieceInfo; +using libpentobi_base::Point; +using libpentobi_base::PointList; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +/** Compute move features for the playout policy. + This class encodes features that correspond to points on the board in bit + ranges of an integer, such that the sum of the features values for all + points of a move can be quickly computed in the playout move generation. + Currently, there are only two features: the forbidden status and whether + the point is a local point. Local points are attach points of recent + opponent moves or points that are adjacent to them. Local points that + are attach points of the color to play count double. + During a simulation, some of the features are updated incrementally + (forbidden status) and some non-incrementally (local points). */ +class PlayoutFeatures +{ +public: + typedef unsigned IntType; + + /** The maximum number of local points for a move. + The number can be higher than PieceInfo::max_size (see class + description). */ + static const unsigned max_local = 2 * PieceInfo::max_size; + + /** Compute the sum of the feature values for a move. */ + class Compute + { + public: + /** Constructor. + @param p The first point of the move + @param playout_features */ + Compute(Point p, const PlayoutFeatures& playout_features) + : m_value(playout_features.m_point_value[p]) + { } + + /** Add a point of the move. */ + void add(Point p, const PlayoutFeatures& playout_features) + { + m_value += playout_features.m_point_value[p]; + } + + bool is_forbidden() const + { + return (m_value & 0xf000u) != 0; + } + + /** Get the number of local points for this move. + @pre ! is_forbidden() + @return The number of local points in [0..max_local] */ + IntType get_nu_local() const + { + LIBBOARDGAME_ASSERT(! is_forbidden()); + return m_value; + } + + private: + IntType m_value; + }; + + friend class Compute; + + PlayoutFeatures(); + + /** Initialize snapshot with forbidden state. */ + void init_snapshot(const Board& bd, Color c); + + void restore_snapshot(const Board& bd); + + /** Set points of move to forbidden. */ + template + void set_forbidden(const MoveInfo& info); + + /** Set adjacent points of move to forbidden. */ + template + void set_forbidden(const MoveInfoExt& info_ext); + + template + void set_local(const Board& bd); + +private: + GridExt m_point_value; + + Grid m_snapshot; + + /** Points with non-zero local value. */ + PointList m_local_points; +}; + +inline void PlayoutFeatures::init_snapshot(const Board& bd, Color c) +{ + m_point_value[Point::null()] = 0; + auto& is_forbidden = bd.is_forbidden(c); + for (Point p : bd) + m_snapshot[p] = (is_forbidden[p] ? 0x1000u : 0); +} + + +inline void PlayoutFeatures::restore_snapshot(const Board& bd) +{ + m_point_value.copy_from(m_snapshot, bd.get_geometry()); +} + +template +inline void PlayoutFeatures::set_forbidden(const MoveInfo& info) +{ + auto p = info.begin(); + for (unsigned i = 0; i < MAX_SIZE; ++i, ++p) + m_point_value[*p] = 0x1000u; + m_point_value[Point::null()] = 0; +} + +template +inline void PlayoutFeatures::set_forbidden( + const MoveInfoExt& info_ext) +{ + for (auto i = info_ext.begin_adj(), end = info_ext.end_adj(); i != end; + ++i) + m_point_value[*i] = 0x1000u; +} + +template +inline void PlayoutFeatures::set_local(const Board& bd) +{ + // Clear old info about local points + for (Point p : m_local_points) + m_point_value[p] &= 0xf000u; + unsigned nu_local = 0; + + Color to_play = bd.get_to_play(); + Color second_color; + if (bd.get_variant() == Variant::classic_3 && to_play.to_int() == 3) + second_color = Color(bd.get_alt_player()); + else + second_color = bd.get_second_color(to_play); + auto& geo = bd.get_geometry(); + auto& moves = bd.get_moves(); + auto move_info_ext_array = bd.get_board_const().get_move_info_ext_array(); + // Consider last 3 moves for local points (i.e. last 2 opponent moves in + // two-color variants) + auto end = moves.end(); + auto begin = (end - moves.begin() < 3 ? moves.begin() : end - 3); + for (auto i = begin; i != end; ++i) + { + Color c = i->color; + if (c == to_play || c == second_color) + continue; + Move mv = i->move; + auto& is_forbidden = bd.is_forbidden(c); + auto& info_ext = BoardConst::get_move_info_ext( + mv, move_info_ext_array); + auto j = info_ext.begin_attach(); + auto end = info_ext.end_attach(); + do + { + if (is_forbidden[*j]) + continue; + if (m_point_value[*j] == 0) + { + m_local_points.get_unchecked(nu_local++) = *j; + m_point_value[*j] = + 1 + static_cast( + bd.is_attach_point(*j, to_play)); + } + if (MAX_SIZE == 7) // Nexos + LIBBOARDGAME_ASSERT(geo.get_adj(*j).empty()); + else + for (Point k : geo.get_adj(*j)) + if (! is_forbidden[k] && m_point_value[k] == 0) + { + m_local_points.get_unchecked(nu_local++) = k; + m_point_value[k] = + 1 + static_cast( + bd.is_attach_point(k, to_play)); + } + } + while (++j != end); + } + m_local_points.resize(nu_local); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts + +#endif // LIBPENTOBI_MCTS_PLAYOUT_FEATURES_H diff --git a/src/libpentobi_mcts/PriorKnowledge.cpp b/src/libpentobi_mcts/PriorKnowledge.cpp new file mode 100644 index 0000000..369347e --- /dev/null +++ b/src/libpentobi_mcts/PriorKnowledge.cpp @@ -0,0 +1,132 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/PriorKnowledge.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "PriorKnowledge.h" + +#include +#include "libboardgame_util/MathUtil.h" + +namespace libpentobi_mcts { + +using libboardgame_util::fast_exp; +using libpentobi_base::BoardType; +using libpentobi_base::Color; +using libpentobi_base::PointState; +using libpentobi_base::PieceInfo; +using libpentobi_base::PieceSet; + +//----------------------------------------------------------------------------- + +PriorKnowledge::PriorKnowledge() +{ + m_is_local.fill_all(false); +} + +void PriorKnowledge::start_search(const Board& bd) +{ + auto& geo = bd.get_geometry(); + auto board_type = bd.get_board_type(); + auto piece_set = bd.get_piece_set(); + + // Init m_dist_to_center + float width = static_cast(geo.get_width()); + float height = static_cast(geo.get_height()); + float center_x = 0.5f * width - 0.5f; + float center_y = 0.5f * height - 0.5f; + bool is_trigon = (piece_set == PieceSet::trigon); + float ratio = (is_trigon ? 1.732f : 1); + for (Point p : geo) + { + float x = static_cast(geo.get_x(p)); + float y = static_cast(geo.get_y(p)); + float dx = x - center_x; + float dy = ratio * (y - center_y); + float d = sqrt(dx * dx + dy * dy); + if (board_type == BoardType::classic) + // Don't make a distinction between moves close enough to the + // center in game variant Classic/Classic2 + d = max(d, 2.f); + m_dist_to_center[p] = d; + } + m_dist_to_center[Point::null()] = numeric_limits::max(); + + // Init m_check_dist_to_center + switch(bd.get_variant()) + { + case Variant::classic: + case Variant::classic_2: + m_check_dist_to_center.fill(true); + m_dist_to_center_max_pieces = 12; + m_max_dist_diff = 0.3f; + break; + case Variant::classic_3: + m_check_dist_to_center.fill(true); + m_dist_to_center_max_pieces = 10; + m_max_dist_diff = 0.3f; + break; + case Variant::trigon: + case Variant::trigon_2: + case Variant::trigon_3: + m_check_dist_to_center.fill(true); + m_dist_to_center_max_pieces = 3; + m_max_dist_diff = 0.5f; + break; + case Variant::duo: + case Variant::junior: + m_check_dist_to_center.fill(false); + break; + case Variant::callisto: + m_check_dist_to_center.fill(true); + m_dist_to_center_max_pieces = 4; + m_max_dist_diff = 0; + break; + case Variant::callisto_2: + m_check_dist_to_center.fill(true); + m_dist_to_center_max_pieces = 4; + m_max_dist_diff = 0; + break; + case Variant::callisto_3: + m_check_dist_to_center.fill(true); + m_dist_to_center_max_pieces = 3; + m_max_dist_diff = 0; + break; + case Variant::nexos: + case Variant::nexos_2: + m_check_dist_to_center.fill(true); + m_dist_to_center_max_pieces = 7; + m_max_dist_diff = 0.3f; + break; + } + + if (piece_set != PieceSet::callisto) + // Don't check dist to center if the position was setup in a way that + // placed pieces but did not cover the starting point(s), otherwise the + // search might not generate any moves (if no moves meet the + // dist-to-center condition). Even if such positions cannot occur in + // legal games, we still don't want the move generation to fail. + for (Color c : bd.get_colors()) + { + if (bd.get_nu_onboard_pieces(c) == 0) + continue; + bool is_starting_point_covered = false; + for (Point p : bd.get_starting_points(c)) + if (bd.get_point_state(p) == PointState(c)) + { + is_starting_point_covered = true; + break; + } + if (! is_starting_point_covered) + m_check_dist_to_center[c] = false; + } +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts diff --git a/src/libpentobi_mcts/PriorKnowledge.h b/src/libpentobi_mcts/PriorKnowledge.h new file mode 100644 index 0000000..6bfae56 --- /dev/null +++ b/src/libpentobi_mcts/PriorKnowledge.h @@ -0,0 +1,432 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/PriorKnowledge.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_MCTS_PRIOR_KNOWLEDGE_H +#define LIBPENTOBI_MCTS_PRIOR_KNOWLEDGE_H + +#include "Float.h" +#include "SearchParamConst.h" +#include "libboardgame_mcts/Tree.h" +#include "libboardgame_util/MathUtil.h" +#include "libpentobi_base/Board.h" + +namespace libpentobi_mcts { + +using namespace std; +using libboardgame_util::fast_exp; +using libpentobi_base::Board; +using libpentobi_base::BoardConst; +using libpentobi_base::ColorMap; +using libpentobi_base::ColorMove; +using libpentobi_base::Grid; +using libpentobi_base::GridExt; +using libpentobi_base::Move; +using libpentobi_base::MoveList; +using libpentobi_base::Point; +using libpentobi_base::PointList; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +/** Initializes newly created nodes with heuristic prior count and value. */ +class PriorKnowledge +{ +public: + typedef libboardgame_mcts::Node + Node; + + typedef libboardgame_mcts::Tree Tree; + + PriorKnowledge(); + + void start_search(const Board& bd); + + /** Generate children nodes initialized with prior knowledge. + @return false If the tree has not enough capacity for the children. */ + template + bool gen_children(const Board& bd, const MoveList& moves, + bool is_symmetry_broken, Tree::NodeExpander& expander, + Float root_val); + +private: + struct MoveFeatures + { + /** Heuristic value of the move expressed in score points. */ + Float heuristic; + + bool is_local; + + /** Does the move touch a piece of the same player? */ + bool connect; + + /** Only used on Classic and Trigon boards. */ + float dist_to_center; + }; + + + array m_features; + + /** Maximum of Features::heuristic for all moves. */ + Float m_max_heuristic; + + bool m_has_connect_move; + + ColorMap m_check_dist_to_center; + + unsigned m_dist_to_center_max_pieces; + + float m_min_dist_to_center; + + float m_max_dist_diff; + + /** Marker for attach points of recent opponent moves. */ + GridExt m_is_local; + + /** Points in m_is_local with value greater zero. */ + PointList m_local_points; + + /** Distance to center heuristic. */ + GridExt m_dist_to_center; + + + template + void compute_features(const Board& bd, const MoveList& moves, + bool check_dist_to_center, bool check_connect); + + template + void init_local(const Board& bd); +}; + + +template +void PriorKnowledge::compute_features(const Board& bd, const MoveList& moves, + bool check_dist_to_center, + bool check_connect) +{ + auto to_play = bd.get_to_play(); + auto variant = bd.get_variant(); + Color second_color; + // connect_color is the 2nd color of the player in game variants with 2 + // colors per player (connecting to_play and connect_color is good) and + // to_play in other game variants (which disables the bonus without + // needing an extra check below because adj_point_value is not used for + // pieces of to_play because it is illegal for to_play to play there). + Color connect_color; + if (variant == Variant::classic_3 && to_play.to_int() == 3) + { + second_color = Color(bd.get_alt_player()); + connect_color = to_play; + } + else + { + second_color = bd.get_second_color(to_play); + connect_color = second_color; + } + auto& bc = bd.get_board_const(); + auto& geo = bc.get_geometry(); + auto move_info_array = bc.get_move_info_array(); + auto move_info_ext_array = bc.get_move_info_ext_array(); + auto& is_forbidden = bd.is_forbidden(to_play); + GridExt point_value; + point_value[Point::null()] = 0; + Grid attach_point_value; + Grid adj_point_value; + for (Point p : geo) + { + auto s = bd.get_point_state(p); + if (is_forbidden[p]) + { + if (s != to_play) + attach_point_value[p] = -2.5; + else + attach_point_value[p] = 0.5; + if (s == connect_color) + // Connecting own colors is good + adj_point_value[p] = 1; + else if (! s.is_empty()) + // Touching opponent is better than playing elsewhere (no need to + // check if s == to_play, such moves are illegal) + adj_point_value[p] = 0.4f; + else + adj_point_value[p] = 0; + } + else + { + point_value[p] = 1; + attach_point_value[p] = 0.5; + if (bd.is_attach_point(p, to_play)) + // Making own attach point forbidden is especially bad + adj_point_value[p] = -1; + else + // Creating new forbidden points is a bad thing + adj_point_value[p] = -0.1f; + } + } + for (Color c : bd.get_colors()) + { + if (c == to_play || c == second_color) + continue; + auto& is_forbidden = bd.is_forbidden(c); + for (Point p : bd.get_attach_points(c)) + if (! is_forbidden[p]) + { + // Occupying opponent attach points or points adjacent to them + // is very good + point_value[p] = 3.f; + for (Point j : geo.get_adj(p)) + if (! is_forbidden[j]) + point_value[j] = 3.f; + } + } + if (variant == Variant::classic_2 + || (variant == Variant::classic_3 && second_color != to_play)) + { + auto& is_forbidden_second_color = bd.is_forbidden(second_color); + for (Point p : bd.get_attach_points(second_color)) + if (! is_forbidden_second_color[p]) + { + // Occupying attach points of second color is bad + point_value[p] -= 3.f; + if (! is_forbidden[p]) + // Sharing an attach point with second color is bad + attach_point_value[p] -= 1.f; + } + } + m_max_heuristic = -numeric_limits::max(); + m_min_dist_to_center = numeric_limits::max(); + m_has_connect_move = false; + for (unsigned i = 0; i < moves.size(); ++i) + { + auto mv = moves[i]; + auto info = BoardConst::get_move_info(mv, move_info_array); + auto& info_ext = BoardConst::get_move_info_ext( + mv, move_info_ext_array); + auto& features = m_features[i]; + auto j = info.begin(); + Float heuristic = point_value[*j]; + bool local = m_is_local[*j]; + if (! check_dist_to_center) + for (unsigned k = 1; k < MAX_SIZE; ++k) + { + ++j; + heuristic += point_value[*j]; + // Logically, we mean: local = local || m_is_local[*j] + // But this generates branches, which are bad for performance + // in this tight loop (unrolled by the compiler). So we use a + // bitwise OR, which works because C++ guarantees that + // true/false converts to 1/0. + local |= m_is_local[*j]; + } + else + { + features.dist_to_center = m_dist_to_center[*j]; + for (unsigned k = 1; k < MAX_SIZE; ++k) + { + ++j; + heuristic += point_value[*j]; + // See comment above about bitwise OR on bool + local |= m_is_local[*j]; + features.dist_to_center = + min(features.dist_to_center, m_dist_to_center[*j]); + } + m_min_dist_to_center = + min(m_min_dist_to_center, features.dist_to_center); + } + j = info_ext.begin_attach(); + auto end = info_ext.end_attach(); + heuristic += attach_point_value[*j]; + while (++j != end) + heuristic += attach_point_value[*j]; + if (MAX_SIZE == 7) // Nexos + { + LIBBOARDGAME_ASSERT(info_ext.size_adj_points == 0); + LIBBOARDGAME_ASSERT(! check_connect); + } + else + { + j = info_ext.begin_adj(); + end = info_ext.end_adj(); + if (! check_connect) + { + for ( ; j != end; ++j) + heuristic += adj_point_value[*j]; + } + else + { + features.connect = (bd.get_point_state(*j) == second_color); + for ( ; j != end; ++j) + { + heuristic += adj_point_value[*j]; + if (bd.get_point_state(*j) == second_color) + features.connect = true; + } + if (features.connect) + m_has_connect_move = true; + } + } + if (heuristic > m_max_heuristic) + m_max_heuristic = heuristic; + features.heuristic = heuristic; + features.is_local = local; + } +} + +template +bool PriorKnowledge::gen_children(const Board& bd, const MoveList& moves, + bool is_symmetry_broken, + Tree::NodeExpander& expander, Float root_val) +{ + if (moves.empty()) + { + // Add a pass move. The initialization value does not matter for a + // single child, but we need to use SearchParamConst::child_min_count + // for the count to avoid an assertion. + if (! expander.check_capacity(1)) + return false; + expander.add_child(Move::null(), root_val, 3); + return true; + } + init_local(bd); + auto to_play = bd.get_to_play(); + auto nu_onboard_pieces = bd.get_nu_onboard_pieces(); + bool check_dist_to_center = + (m_check_dist_to_center[to_play] + && nu_onboard_pieces <= m_dist_to_center_max_pieces); + bool check_connect = + (bd.get_variant() == Variant::classic_2 && nu_onboard_pieces < 14); + compute_features(bd, moves, check_dist_to_center, + check_connect); + if (! m_has_connect_move) + check_connect = false; + Move symmetric_mv = Move::null(); + bool has_symmetry_breaker = false; + if (! is_symmetry_broken) + { + unsigned nu_moves = bd.get_nu_moves(); + if (to_play == Color(1) || to_play == Color(3)) + { + if (nu_moves > 0) + { + ColorMove last = bd.get_move(nu_moves - 1); + symmetric_mv = + bd.get_move_info_ext_2(last.move).symmetric_move; + } + } + else if (nu_moves > 0) + for (Move mv : moves) + if (bd.get_move_info_ext_2(mv).breaks_symmetry) + { + has_symmetry_breaker = true; + break; + } + } + m_min_dist_to_center += m_max_dist_diff; + if (! expander.check_capacity(static_cast(moves.size()))) + return false; + for (unsigned i = 0; i < moves.size(); ++i) + { + const auto& features = m_features[i]; + + // Depending on the game variant, prune early moves that don't minimize + // dist to center and moves that don't connect in the middle if + // connection is possible + if ((check_dist_to_center + && features.dist_to_center > m_min_dist_to_center) + || (check_connect && ! features.connect)) + continue; + + auto mv = moves[i]; + + // Convert the heuristic, which is so far estimated in score points, + // into a win/loss value in [0..1] by making it relative to the + // heuristic of the best move and let it decrease exponentially with a + // certain width. We could use exp(-c*x) here, but we use + // 0.1+0.9*exp(-c*x) instead to avoid that the value is too close to + // 0, because then it might never get explored in practice if the bias + // term constant is small. + Float heuristic = m_max_heuristic - features.heuristic; + heuristic = 0.1f + 0.9f * fast_exp(-0.6f * heuristic); + + // Initialize value from heuristic and root_val, each with a count + // of 1.5. If this is changed, SearchParamConst::child_min_count + // should be updated. + Float value = 1.5f * (heuristic + root_val); + Float count = 3; + + // If a symmetric draw is still possible, encourage exploring a move + // that keeps or breaks the symmetry by adding 5 wins or 5 losses + // See also the comment in evaluate_playout() + if (! symmetric_mv.is_null()) + { + if (mv == symmetric_mv) + value += 5; + count += 5; + } + else if (has_symmetry_breaker + && ! bd.get_move_info_ext_2(mv).breaks_symmetry) + continue; + + // Add 1 win for moves that are local responses to recent opponent + // moves + if (features.is_local) + { + value += 1; + count += 1; + } + + LIBBOARDGAME_ASSERT(bd.is_legal(to_play, mv)); + expander.add_child(mv, value / count, count); + } + return true; +} + +template +inline void PriorKnowledge::init_local(const Board& bd) +{ + for (Point p : m_local_points) + m_is_local[p] = false; + unsigned nu_local = 0; + Color to_play = bd.get_to_play(); + Color second_color; + if (bd.get_variant() == Variant::classic_3 && to_play.to_int() == 3) + second_color = Color(bd.get_alt_player()); + else + second_color = bd.get_second_color(to_play); + auto& moves = bd.get_moves(); + auto move_info_ext_array = bd.get_board_const().get_move_info_ext_array(); + // Consider last 3 moves for local points (i.e. last 2 opponent moves in + // two-color variants) + auto end = moves.end(); + auto begin = (end - moves.begin() < 3 ? moves.begin() : end - 3); + for (auto i = begin; i != end; ++i) + { + Color c = i->color; + if (c == to_play || c == second_color) + continue; + auto mv = i->move; + auto& is_forbidden = bd.is_forbidden(c); + auto& info_ext = BoardConst::get_move_info_ext( + mv, move_info_ext_array); + auto j = info_ext.begin_attach(); + auto end = info_ext.end_attach(); + do + { + if (is_forbidden[*j]) + continue; + if (! m_is_local[*j]) + m_local_points.get_unchecked(nu_local++) = *j; + m_is_local[*j] = true; + } + while (++j != end); + } + m_local_points.resize(nu_local); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts + +#endif // LIBPENTOBI_MCTS_PRIOR_KNOWLEDGE_H diff --git a/src/libpentobi_mcts/Search.cpp b/src/libpentobi_mcts/Search.cpp new file mode 100644 index 0000000..26e1e88 --- /dev/null +++ b/src/libpentobi_mcts/Search.cpp @@ -0,0 +1,171 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/Search.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Search.h" + +#include "Util.h" + +namespace libpentobi_mcts { + +//----------------------------------------------------------------------------- + +Search::Search(Variant initial_variant, unsigned nu_threads, size_t memory) + : SearchBase(nu_threads == 0 ? util::get_nu_threads() : nu_threads, + memory), + m_auto_param(true), + m_variant(initial_variant), + m_shared_const(m_to_play) +{ + set_default_param(m_variant); + create_threads(); +} + +Search::~Search() = default; + +bool Search::check_followup(ArrayList& sequence) +{ + auto& bd = get_board(); + m_history.init(bd, m_to_play); + bool is_followup = m_history.is_followup(m_last_history, sequence); + + // If avoid_symmetric_draw is enabled, class State uses a different + // evaluation function depending on which player is to play in the root + // position (the first player knows about symmetric draws to be able to + // play a symmetry breaker but the second player pretends not to know about + // symmetric draws to avoid going for such a draw). In this case, we cannot + // reuse parts of the old search tree if the computer plays both colors. + if (m_shared_const.avoid_symmetric_draw + && is_followup && m_to_play != m_last_history.get_to_play() + && has_central_symmetry(bd.get_variant()) + && ! check_symmetry_broken(bd)) + is_followup = false; + + m_last_history = m_history; + return is_followup; +} + +unique_ptr Search::create_state() +{ + return unique_ptr(new State(m_variant, m_shared_const)); +} + +void Search::get_root_position(Variant& variant, Setup& setup) const +{ + m_last_history.get_as_setup(variant, setup); + setup.to_play = m_to_play; +} + +void Search::on_start_search(bool is_followup) +{ + m_shared_const.init(is_followup); +} + +bool Search::search(Move& mv, const Board& bd, Color to_play, + Float max_count, size_t min_simulations, + double max_time, TimeSource& time_source) +{ + m_shared_const.board = &bd; + m_to_play = to_play; + auto variant = bd.get_variant(); + if (m_auto_param && variant != m_variant) + set_default_param(variant); + m_variant = variant; + bool result = SearchBase::search(mv, max_count, min_simulations, max_time, + time_source); + // Search doesn't generate all useless one-piece moves in Callisto + if (result && mv.is_null() && bd.get_piece_set() == PieceSet::callisto + && bd.is_piece_left(to_play, bd.get_one_piece())) + { + for (Point p : bd) + if (! bd.is_forbidden(p, to_play) && ! bd.is_center_section(p)) + { + auto moves = bd.get_board_const().get_moves(bd.get_one_piece(), + p, 0); + LIBBOARDGAME_ASSERT(moves.size() == 1); + mv = *moves.begin(); + result = true; + break; + } + } + return result; +} + +void Search::set_default_param(Variant variant) +{ + LIBBOARDGAME_LOG("Setting default parameters for ", to_string(variant)); + set_expand_threshold(1); + set_expand_threshold_inc(0.5f); + set_rave_weight(0.7f); + set_rave_child_max(2000); + // The following parameters are currently tuned for duo, classic_2 and + // trigon_2 and used for all other game variants with the same board type + switch (variant) + { + case Variant::classic: + case Variant::classic_2: + case Variant::classic_3: + set_exploration_constant(0.021f); + set_rave_parent_max(50000); + break; + case Variant::duo: + case Variant::junior: + set_exploration_constant(0.020f); + set_rave_parent_max(25000); + break; + case Variant::trigon: + case Variant::trigon_2: + case Variant::trigon_3: + case Variant::callisto: + case Variant::callisto_3: + set_exploration_constant(0.014f); + set_rave_parent_max(50000); + break; + case Variant::nexos: + case Variant::nexos_2: + set_exploration_constant(0.008f); + set_rave_parent_max(50000); + break; + case Variant::callisto_2: + set_exploration_constant(0.011f); + set_rave_parent_max(25000); + break; + } +} + +string Search::get_info() const +{ + if (get_nu_simulations() == 0) + return string(); + auto& root = get_tree().get_root(); + if (! root.has_children()) + return string(); + ostringstream s; + s << SearchBase::get_info() + << "Mov: " << root.get_nu_children() << ", "; + if (libpentobi_base::get_nu_players(m_variant) > 2) + { + s << "All:"; + for (PlayerInt i = 0; i < libpentobi_base::get_nu_colors(m_variant); + ++i) + { + if (get_root_val(i).get_count() == 0) + s << " -"; + else + s << " " << setprecision(2) << get_root_val(i).get_mean(); + } + s << ", "; + } + s << get_state(0).get_info(); + return s.str(); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts diff --git a/src/libpentobi_mcts/Search.h b/src/libpentobi_mcts/Search.h new file mode 100644 index 0000000..e1d41cb --- /dev/null +++ b/src/libpentobi_mcts/Search.h @@ -0,0 +1,162 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/Search.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_MCTS_SEARCH_H +#define LIBPENTOBI_MCTS_SEARCH_H + +#include "History.h" +#include "SearchParamConst.h" +#include "State.h" +#include "libboardgame_mcts/SearchBase.h" + +namespace libpentobi_mcts { + +using namespace std; +using libboardgame_mcts::PlayerInt; +using libboardgame_util::TimeSource; +using libpentobi_base::Setup; + +//----------------------------------------------------------------------------- + +/** Monte-Carlo tree search implementation for Blokus. + Multiple colors per player (e.g. in Classic 2) are handled by using the + same game result for each color of a player. + Multiple players of a color (the 4th color in Classic 3) are handled by + adding additional players for each player of this color that share the + game result with the main color of the player. + The maximum number of players is 6, which occurs in Classic 3 with 3 + real players and 3 pseudo-players for the 4th color. + @note @ref libboardgame_avoid_stack_allocation */ +class Search final + : public libboardgame_mcts::SearchBase +{ +public: + Search(Variant initial_variant, unsigned nu_threads, size_t memory); + + ~Search(); + + unique_ptr create_state() override; + + PlayerInt get_nu_players() const override; + + PlayerInt get_player() const override; + + bool check_followup(ArrayList& sequence) override; + + string get_info() const override; + + + /** @name Parameters */ + /** @{ */ + + bool get_avoid_symmetric_draw() const; + + void set_avoid_symmetric_draw(bool enable); + + /** Automatically set some user-changeable parameters that have different + optimal values for different game variants whenever the game variant + changes. + Default is true. */ + bool get_auto_param() const; + + void set_auto_param(bool enable); + + /** @} */ // @name + + + bool search(Move& mv, const Board& bd, Color to_play, Float max_count, + size_t min_simulations, double max_time, + TimeSource& time_source); + + /** Get color to play at root node of the last search. */ + Color get_to_play() const; + + const History& get_last_history() const; + + /** Get board position of last search at root node as setup. + @param[out] variant + @param[out] setup */ + void get_root_position(Variant& variant, Setup& setup) const; + +protected: + void on_start_search(bool is_followup) override; + +private: + /** Automatically set default parameters for the game variant if + the game variant changes. */ + bool m_auto_param; + + /** Game variant of last search. */ + Variant m_variant; + + Color m_to_play; + + SharedConst m_shared_const; + + /** Local variable reused for efficiency. */ + History m_history; + + History m_last_history; + + const Board& get_board() const; + + void set_default_param(Variant variant); +}; + +inline bool Search::get_auto_param() const +{ + return m_auto_param; +} + +inline bool Search::get_avoid_symmetric_draw() const +{ + return m_shared_const.avoid_symmetric_draw; +} + +inline const Board& Search::get_board() const +{ + return *m_shared_const.board; +} + +inline const History& Search::get_last_history() const +{ + return m_last_history; +} + +inline PlayerInt Search::get_nu_players() const +{ + return m_variant != Variant::classic_3 ? get_board().get_nu_colors() : 6; +} + +inline PlayerInt Search::get_player() const +{ + auto to_play = m_to_play.to_int(); + if ( m_variant == Variant::classic_3 && to_play == 3) + return static_cast(to_play + get_board().get_alt_player()); + else + return to_play; +} + +inline Color Search::get_to_play() const +{ + return m_to_play; +} + +inline void Search::set_auto_param(bool enable) +{ + m_auto_param = enable; +} + +inline void Search::set_avoid_symmetric_draw(bool enable) +{ + m_shared_const.avoid_symmetric_draw = enable; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts + +#endif // LIBPENTOBI_MCTS_SEARCH_H diff --git a/src/libpentobi_mcts/SearchParamConst.h b/src/libpentobi_mcts/SearchParamConst.h new file mode 100644 index 0000000..aaa8558 --- /dev/null +++ b/src/libpentobi_mcts/SearchParamConst.h @@ -0,0 +1,75 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/SearchParamConst.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_MCTS_SEARCH_PARAM_CONST_H +#define LIBPENTOBI_MCTS_SEARCH_PARAM_CONST_H + +#include "Float.h" +#include "libpentobi_base/Board.h" +#include "libboardgame_mcts/PlayerMove.h" + +namespace libpentobi_mcts { + +using libboardgame_mcts::PlayerInt; +using libpentobi_base::Board; +using libpentobi_base::Color; + +//----------------------------------------------------------------------------- + +/** Optional compile-time parameters for libboardgame_mcts::Search. + See libboardgame_mcts::SearchParamConstDefault for the meaning of the + members. */ +struct SearchParamConst +{ + typedef libpentobi_mcts::Float Float; + + static const PlayerInt max_players = 6; + + /** The maximum number of moves in a simulation. + This needs to include pass moves because in the in-tree phase pass + moves (Move::null()) are used. The game ends after all colors have + passed in a row. Therefore, the maximum number of moves is reached in + case that a piece move is followed by (Color::range-1) pass moves and + an extra Color::range pass moves at the end. */ + static const unsigned max_moves = + Color::range * (Color::range * Board::max_pieces + 1); + +#ifdef LIBBOARDGAME_MCTS_SINGLE_THREAD + static const bool multithread = false; +#else + static const bool multithread = true; +#endif + + static const bool rave = true; + + static const bool rave_dist_weighting = true; + + static const bool use_lgr = true; + +#if PENTOBI_LOW_RESOURCES + static const size_t lgr_hash_table_size = (1 << 20); +#else + static const size_t lgr_hash_table_size = (1 << 21); +#endif + + static const bool virtual_loss = true; + + static const bool use_unlikely_change = true; + + static constexpr Float child_min_count = 3; + + static constexpr Float tie_value = 0.5f; + + static constexpr Float prune_count_start = 16; + + static constexpr double expected_sim_per_sec = 100; +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts + +#endif // LIBPENTOBI_MCTS_SEARCH_PARAM_CONST_H diff --git a/src/libpentobi_mcts/SharedConst.cpp b/src/libpentobi_mcts/SharedConst.cpp new file mode 100644 index 0000000..906d7da --- /dev/null +++ b/src/libpentobi_mcts/SharedConst.cpp @@ -0,0 +1,317 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/SharedConst.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "SharedConst.h" + +namespace libpentobi_mcts { + +using libpentobi_base::BoardConst; +using libpentobi_base::BoardType; +using libpentobi_base::Piece; +using libpentobi_base::PieceSet; +using libpentobi_base::ScoreType; + +//----------------------------------------------------------------------------- + +namespace { + +void filter_min_size(const BoardConst& bc, ScoreType min_size, + PieceMap& is_piece_considered) +{ + for (Piece::IntType i = 0; i < bc.get_nu_pieces(); ++i) + { + Piece piece(i); + auto& piece_info = bc.get_piece_info(piece); + if (piece_info.get_score_points() < min_size) + is_piece_considered[piece] = false; + } +} + +/** Check if an adjacent status is a possible follow-up status for another + one. */ +inline bool is_followup_adj_status(unsigned status_new, unsigned status_old) +{ + return (status_new & status_old) == status_old; +} + +void set_piece_considered(const BoardConst& bc, const char* name, + PieceMap& is_piece_considered, + bool is_considered = true) +{ + Piece piece; + bool found = bc.get_piece_by_name(name, piece); + LIBBOARDGAME_UNUSED_IF_NOT_DEBUG(found); + LIBBOARDGAME_ASSERT(found); + is_piece_considered[piece] = is_considered; +} + +void set_pieces_considered(const Board& bd, unsigned nu_moves, + PieceMap& is_piece_considered) +{ + auto& bc = bd.get_board_const(); + unsigned nu_colors = bd.get_nu_colors(); + is_piece_considered.fill(true); + switch (bc.get_board_type()) + { + case BoardType::duo: + if (nu_moves < 2 * nu_colors) + filter_min_size(bc, 5, is_piece_considered); + else if (nu_moves < 3 * nu_colors) + filter_min_size(bc, 4, is_piece_considered); + else if (nu_moves < 5 * nu_colors) + filter_min_size(bc, 3, is_piece_considered); + break; + case BoardType::classic: + if (nu_moves < nu_colors) + { + is_piece_considered.fill(false); + set_piece_considered(bc, "V5", is_piece_considered); + set_piece_considered(bc, "Z5", is_piece_considered); + } + else if (nu_moves < 2 * nu_colors) + { + filter_min_size(bc, 5, is_piece_considered); + set_piece_considered(bc, "F", is_piece_considered, false); + set_piece_considered(bc, "P", is_piece_considered, false); + set_piece_considered(bc, "T5", is_piece_considered, false); + set_piece_considered(bc, "U", is_piece_considered, false); + set_piece_considered(bc, "X", is_piece_considered, false); + } + else if (nu_moves < 3 * nu_colors) + { + filter_min_size(bc, 5, is_piece_considered); + set_piece_considered(bc, "P", is_piece_considered, false); + set_piece_considered(bc, "U", is_piece_considered, false); + } + else if (nu_moves < 5 * nu_colors) + filter_min_size(bc, 4, is_piece_considered); + else if (nu_moves < 7 * nu_colors) + filter_min_size(bc, 3, is_piece_considered); + break; + case BoardType::trigon: + case BoardType::trigon_3: + if (nu_moves < nu_colors) + { + is_piece_considered.fill(false); + set_piece_considered(bc, "V", is_piece_considered); + set_piece_considered(bc, "I6", is_piece_considered); + } + if (nu_moves < 4 * nu_colors) + { + filter_min_size(bc, 6, is_piece_considered); + // O is a bad early move, it neither extends, nor blocks well + set_piece_considered(bc, "O", is_piece_considered, false); + } + else if (nu_moves < 5 * nu_colors) + filter_min_size(bc, 5, is_piece_considered); + else if (nu_moves < 7 * nu_colors) + filter_min_size(bc, 4, is_piece_considered); + else if (nu_moves < 9 * nu_colors) + filter_min_size(bc, 3, is_piece_considered); + break; + case BoardType::nexos: + if (nu_moves < 3 * nu_colors) + filter_min_size(bc, 4, is_piece_considered); + else if (nu_moves < 5 * nu_colors) + filter_min_size(bc, 3, is_piece_considered); + break; + case BoardType::callisto: + case BoardType::callisto_2: + case BoardType::callisto_3: + is_piece_considered[bd.get_one_piece()] = false; + if (nu_moves < 3 * nu_colors) + filter_min_size(bc, 5, is_piece_considered); + else if (nu_moves < 8 * nu_colors) + filter_min_size(bc, 4, is_piece_considered); + else if (nu_moves < 12 * nu_colors) + filter_min_size(bc, 3, is_piece_considered); + break; + } +} + +} // namespace + +//----------------------------------------------------------------------------- + +SharedConst::SharedConst(const Color& to_play) + : board(nullptr), + to_play(to_play), + avoid_symmetric_draw(true) +{ } + +void SharedConst::init(bool is_followup) +{ + auto& bd = *board; + auto& bc = bd.get_board_const(); + + // Initialize precomp_moves + for (Color c : bd.get_colors()) + { + auto& precomp = precomp_moves[c]; + auto& old_precomp = (is_followup ? precomp : bc.get_precomp_moves()); + + m_is_forbidden.set(); + for (Point p : bd) + if (! bd.is_forbidden(p, c)) + { + auto adj_status = bd.get_adj_status(p, c); + for (Piece piece : bd.get_pieces_left(c)) + { + if (! old_precomp.has_moves(piece, p, adj_status)) + continue; + for (Move mv : old_precomp.get_moves(piece, p, adj_status)) + if (m_is_forbidden[mv] && ! bd.is_forbidden(c, mv)) + m_is_forbidden.clear(mv); + } + } + + // Don't use bd.get_pieces_left() because its ordering is not preserved + // during a game. The in-place construction requires that the loop + // iterates in the same order as during the last construction such that + // it doesn't overwrite elements it still needs to read. + Board::PiecesLeftList pieces; + for (Piece::IntType i = 0; i < bc.get_nu_pieces(); ++i) + if (bd.is_piece_left(c, Piece(i))) + pieces.push_back(Piece(i)); + if (! is_followup) + for (Point p : bd) + if (! bd.is_forbidden(p, c)) + { + auto adj_status = bd.get_adj_status(p, c); + for (unsigned i = 0; i < PrecompMoves::nu_adj_status; ++i) + if (is_followup_adj_status(i, adj_status)) + for (auto piece : pieces) + precomp.set_list_range(p, i, piece, 0, 0); + } + unsigned n = 0; + for (Point p : bd) + { + if (bd.is_forbidden(p, c)) + continue; + auto adj_status = bd.get_adj_status(p, c); + for (unsigned i = 0; i < PrecompMoves::nu_adj_status; ++i) + { + if (! is_followup_adj_status(i, adj_status)) + continue; + for (auto piece : pieces) + { + if (! old_precomp.has_moves(piece, p, i)) + continue; + auto begin = n; + for (auto& mv : old_precomp.get_moves(piece, p, i)) + if (! m_is_forbidden[mv]) + precomp.set_move(n++, mv); + precomp.set_list_range(p, i, piece, begin, n - begin); + } + } + } + } + + if (! is_followup) + init_pieces_considered(); + if (bd.get_piece_set() == PieceSet::callisto) + init_one_piece_callisto(is_followup); +} + +void SharedConst::init_one_piece_callisto(bool is_followup) +{ + auto& bd = *board; + auto& bc = bd.get_board_const(); + Piece one_piece = bd.get_one_piece(); + unsigned n = 0; + if (! is_followup) + { + for (Point p : bd) + if (! bd.is_center_section(p) && bd.get_point_state(p).is_empty()) + { + auto moves = bc.get_moves(one_piece, p, 0); + LIBBOARDGAME_ASSERT(moves.size() == 1); + Move mv = *moves.begin(); + if (! is_useless_one_piece_point(p)) + { + one_piece_points_callisto.get_unchecked(n) = p; + one_piece_moves_callisto.get_unchecked(n) = mv; + ++n; + } + } + } + else + for (unsigned i = 0; i < one_piece_points_callisto.size(); ++i) + { + Point p = one_piece_points_callisto[i]; + Move mv = one_piece_moves_callisto[i]; + if (bd.get_point_state(p).is_empty() + && ! is_useless_one_piece_point(p)) + { + one_piece_points_callisto.get_unchecked(n) = p; + one_piece_moves_callisto.get_unchecked(n) = mv; + ++n; + } + } + one_piece_points_callisto.resize(n); + one_piece_moves_callisto.resize(n); +} + +void SharedConst::init_pieces_considered() +{ + auto& bd = *board; + auto& bc = bd.get_board_const(); + is_piece_considered_list.clear(); + bool is_callisto = (bd.get_piece_set() == PieceSet::callisto); + for (auto i = bd.get_nu_onboard_pieces(); i < Board::max_game_moves; ++i) + { + PieceMap is_piece_considered; + set_pieces_considered(bd, i, is_piece_considered); + bool are_all_considered = true; + for (Piece::IntType j = 0; j < bc.get_nu_pieces(); ++j) + if (! is_piece_considered[Piece(j)] + && ! (is_callisto && Piece(j) == bd.get_one_piece())) + { + are_all_considered = false; + break; + } + if (are_all_considered) + { + min_move_all_considered = i; + break; + } + auto pos = find(is_piece_considered_list.begin(), + is_piece_considered_list.end(), + is_piece_considered); + if (pos != is_piece_considered_list.end()) + this->is_piece_considered[i] = &(*pos); + else + { + is_piece_considered_list.push_back(is_piece_considered); + this->is_piece_considered[i] = &is_piece_considered_list.back(); + } + } + is_piece_considered_all.fill(true); + if (is_callisto) + is_piece_considered_all[bd.get_one_piece()] = false; + is_piece_considered_none.fill(false); +} + +/** Check if a point is a useless move for the 1-piece. + @return true if all neighbors are occupied, because the 1-piece doesn't + contribute to the score and playing there neither enables own moves + nor prevents opponent moves with larger pieces. */ +bool SharedConst::is_useless_one_piece_point(Point p) const +{ + auto& bd = *board; + for (Point pp: bd.get_geometry().get_diag(p)) + if (bd.get_point_state(pp).is_empty()) + return false; + return true; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts diff --git a/src/libpentobi_mcts/SharedConst.h b/src/libpentobi_mcts/SharedConst.h new file mode 100644 index 0000000..a02d776 --- /dev/null +++ b/src/libpentobi_mcts/SharedConst.h @@ -0,0 +1,96 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/SharedConst.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_MCTS_SHARED_CONST_H +#define LIBPENTOBI_MCTS_SHARED_CONST_H + +#include "libpentobi_base/Board.h" +#include "libpentobi_base/MoveMarker.h" + +namespace libpentobi_mcts { + +using namespace std; +using libboardgame_util::ArrayList; +using libpentobi_base::Board; +using libpentobi_base::Color; +using libpentobi_base::ColorMap; +using libpentobi_base::Move; +using libpentobi_base::MoveMarker; +using libpentobi_base::PieceMap; +using libpentobi_base::Point; +using libpentobi_base::PointList; +using libpentobi_base::PrecompMoves; + +//----------------------------------------------------------------------------- + +/** Constant data shared between the search states. */ +class SharedConst +{ +public: + /** Precomputed moves additionally constrained by moves that are + non-forbidden at root position. */ + ColorMap precomp_moves; + + /** The game board. + Contains the current position. */ + const Board* board; + + /** The color to play at the root of the search. */ + const Color& to_play; + + bool avoid_symmetric_draw; + + /** Minimum total number of pieces on the board where all pieces are + considered until the rest of the simulation. */ + unsigned min_move_all_considered; + + /** Precomputed lists of considered pieces depending on the total number + of pieces on the board. + Only initialized for numbers greater than or equal to the number in the + root position and less than min_move_all_considered. + Contains pointers to unique values such that the comparison of the + lists can be done by comparing the pointers to the lists. */ + array*, Board::max_game_moves> is_piece_considered; + + /** List of unique values for is_piece_considered. */ + ArrayList, Board::max_game_moves> is_piece_considered_list; + + /** Precomputed lists of considered pieces if all pieces are enforced to be + considered (because using the restricted set of pieces would generate + no moves). */ + PieceMap is_piece_considered_all; + + PieceMap is_piece_considered_none; + + /** List of legal points in the root position for the 1x1-piece in + Callisto. */ + PointList one_piece_points_callisto; + + /** Moves corresponding to one_piece_points_callisto. */ + ArrayList one_piece_moves_callisto; + + + explicit SharedConst(const Color& to_play); + + void init(bool is_followup); + +private: + /** Temporary variable used in init(). + Reused for efficiency. */ + MoveMarker m_is_forbidden; + + void init_one_piece_callisto(bool is_followup); + + void init_pieces_considered(); + + bool is_useless_one_piece_point(Point p) const; +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts + +#endif // LIBPENTOBI_MCTS_SHARED_CONST_H diff --git a/src/libpentobi_mcts/State.cpp b/src/libpentobi_mcts/State.cpp new file mode 100644 index 0000000..5b3da1c --- /dev/null +++ b/src/libpentobi_mcts/State.cpp @@ -0,0 +1,873 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/State.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "State.h" + +#include "libboardgame_util/MathUtil.h" +#include "libpentobi_base/ScoreUtil.h" +#if LIBBOARDGAME_DEBUG +#include "libpentobi_base/BoardUtil.h" +#endif + +namespace libpentobi_mcts { + +using libboardgame_util::fast_exp; +using libpentobi_base::get_multiplayer_result; +using libpentobi_base::BoardType; +using libpentobi_base::PointState; +using libpentobi_base::ScoreType; + +//----------------------------------------------------------------------------- + +namespace { + +/** Gamma value for PlayoutFeatures::get_nu_local(). + The value of nu_local dominates all other features, so we use a high + gamma. Above some limit, we don't care about the exact value. */ +float gamma_local[PlayoutFeatures::max_local + 1] = + { 1, 1e6f, 1e12f, 1e18f, 1e24f, 1e25f, 1e25f, 1e25f, 1e25f, 1e25f, 1e25f, + 1e25f, 1e25f, 1e25f, 1e25f }; + +inline Float sigmoid(Float steepness, Float x) +{ + return -1.f + 2.f / (1.f + fast_exp(-steepness * x)); +} + +} // namespace + +//----------------------------------------------------------------------------- + +State::State(Variant initial_variant, const SharedConst& shared_const) + : m_shared_const(shared_const), + m_bd(initial_variant) +{ +} + +template +inline void State::add_moves(Point p, Color c, + const Board::PiecesLeftList& pieces, + float& total_gamma, MoveList& moves, + unsigned& nu_moves) +{ + auto& marker = m_marker[c]; + auto& playout_features = m_playout_features[c]; + auto adj_status = m_bd.get_adj_status(p, c); + for (Piece piece : pieces) + { + if (! has_moves(c, piece, p, adj_status)) + continue; + auto gamma_piece = m_gamma_piece[piece]; + for (Move mv : get_moves(c, piece, p, adj_status)) + if (! marker[mv] + && check_move( + mv, get_move_info(mv), gamma_piece, moves, + nu_moves, playout_features, total_gamma)) + marker.set(mv); + } +} + +template +void State::add_one_piece_moves(Color c, bool with_gamma, float& total_gamma, + MoveList& moves, unsigned& nu_moves) +{ + Piece one_piece = m_bd.get_one_piece(); + auto nu_left = m_bd.get_nu_left_piece(c, one_piece); + if (nu_left == 0) + return; + for (unsigned i = 0; i < m_shared_const.one_piece_points_callisto.size(); + ++i) + { + Point p = m_shared_const.one_piece_points_callisto[i]; + if (m_bd.is_forbidden(p, c)) + continue; + Move mv = m_shared_const.one_piece_moves_callisto[i]; + LIBBOARDGAME_ASSERT(nu_moves < MoveList::max_size); + moves.get_unchecked(nu_moves) = mv; + ++nu_moves; + LIBBOARDGAME_ASSERT(! m_marker[c][mv]); + m_marker[c].set(mv); + if (with_gamma) + { + total_gamma += m_gamma_piece[one_piece]; + m_cumulative_gamma[nu_moves - 1] = total_gamma; + } + } +} + +template +void State::add_starting_moves(Color c, const Board::PiecesLeftList& pieces, + bool with_gamma, MoveList& moves) +{ + // Using only one starting point (if game variant has more than one) not + // only reduces the branching factor but is also necessary because + // update_moves() assumes that a move stays legal if the forbidden + // status for all of its points does not change. + Point p = find_best_starting_point(c); + if (p.is_null()) + return; + unsigned nu_moves = 0; + auto& marker = m_marker[c]; + auto& is_forbidden = m_bd.is_forbidden(c); + float total_gamma = 0; + for (Piece piece : pieces) + for (Move mv : get_moves(c, piece, p, 0)) + { + LIBBOARDGAME_ASSERT(! marker[mv]); + if (check_forbidden(is_forbidden, mv, moves, nu_moves)) + { + marker.set(mv); + if (with_gamma) + { + total_gamma += m_gamma_piece[piece]; + m_cumulative_gamma[nu_moves - 1] = total_gamma; + } + } + } + moves.resize(nu_moves); +} + +template +bool State::check_forbidden(const GridExt& is_forbidden, Move mv, + MoveList& moves, unsigned& nu_moves) +{ + auto p = get_move_info(mv).begin(); + unsigned forbidden = is_forbidden[*p]; + for (unsigned i = 1; i < MAX_SIZE; ++i) + // Logically, forbidden is a bool and the next line should be + // forbidden = forbidden || is_forbidden[*(++p)] + // But this generates branches, which are bad for performance in this + // tight loop (unrolled by the compiler). So we use a bitwise OR, which + // works because C++ guarantees that true/false converts to 1/0. + forbidden |= static_cast(is_forbidden[*(++p)]); + if (forbidden != 0) + return false; + LIBBOARDGAME_ASSERT(nu_moves < MoveList::max_size); + moves.get_unchecked(nu_moves) = mv; + ++nu_moves; + return true; +} + +template +bool State::check_move(Move mv, const MoveInfo& info, + float gamma_piece, MoveList& moves, unsigned& nu_moves, + const PlayoutFeatures& playout_features, + float& total_gamma) +{ + LIBBOARDGAME_ASSERT(IS_CALLISTO == m_is_callisto); + auto p = info.begin(); + PlayoutFeatures::Compute features(*p, playout_features); + for (unsigned i = 1; i < MAX_SIZE; ++i) + features.add(*(++p), playout_features); + if (features.is_forbidden()) + return false; + auto gamma = gamma_piece; + if (! (IS_CALLISTO && info.get_size() == 1)) + gamma *= gamma_local[features.get_nu_local()]; + total_gamma += gamma; + m_cumulative_gamma[nu_moves] = total_gamma; + LIBBOARDGAME_ASSERT(nu_moves < MoveList::max_size); + moves.get_unchecked(nu_moves) = mv; + ++nu_moves; + return true; +} + +template +inline bool State::check_move(Move mv, const MoveInfo& info, + MoveList& moves, unsigned& nu_moves, + const PlayoutFeatures& playout_features, + float& total_gamma) +{ + return check_move( + mv, info, m_gamma_piece[info.get_piece()], moves, nu_moves, + playout_features, total_gamma); +} + +#if LIBBOARDGAME_DEBUG +string State::dump() const +{ + ostringstream s; + s << "pentobi_mcts::State:\n" << libpentobi_base::boardutil::dump(m_bd); + return s.str(); +} +#endif + +/** Evaluation function for game variants with 2 players and 2 colors per + player. */ +void State::evaluate_multicolor(array& result) +{ + LIBBOARDGAME_ASSERT(m_bd.get_nu_players() == 2); + LIBBOARDGAME_ASSERT(m_bd.get_nu_colors() == 4); + // Always evaluate symmetric positions in trigon_2 as a draw in the + // playouts. See comment in evaluate_playout_duo. + // m_is_symmetry_broken is always true in classic_2, no need to check for + // game variant. + if (! m_is_symmetry_broken + && m_bd.get_nu_onboard_pieces() >= m_symmetry_min_nu_pieces) + { + if (log_simulations) + LIBBOARDGAME_LOG("Result: 0.5 (symmetry)"); + result[0] = result[1] = result[2] = result[3] = 0.5; + return; + } + + auto s = m_bd.get_score_multicolor(Color(0)); + Float res; + if (s > 0) + res = 1; + else if (s < 0) + res = 0; + else + res = 0.5; + if (log_simulations) + LIBBOARDGAME_LOG("Result color 0: sco=", s, " game_res=", res); + res += get_quality_bonus(Color(0), res, s) + + get_quality_bonus_attach_multicolor(); + if (log_simulations) + LIBBOARDGAME_LOG("res=", res); + result[0] = result[2] = res; + result[1] = result[3] = 1.f - res; +} + +/** Evaluation function for game variants with more than 2 players. + The result is 0,0.5,1 for loss/tie/win in 2-player variants. For n \> 2 + players, this is generalized in the following way: The scores are sorted in + ascending order. Each rank r_i (i in 0..n-1) is assigned a result value of + r_i/(n-1). If multiple players have the same score, the result value is the + average of all ranks with this score. So being the single winner still + gives the result 1 and having the lowest score gives the result 0. Being + the single winner is better than sharing the best place, which is better + than getting the second place, etc. */ +void State::evaluate_multiplayer(array& result) +{ + auto nu_players = m_bd.get_nu_players(); + LIBBOARDGAME_ASSERT(nu_players > 2); + array points; + for (Color::IntType i = 0; i < nu_players; ++i) + points[i] = m_bd.get_points(Color(i)); + array game_result; + get_multiplayer_result(nu_players, points, game_result, m_is_callisto); + for (Color::IntType i = 0; i < nu_players; ++i) + { + Color c(i); + auto s = m_bd.get_score_multiplayer(c); + result[i] = game_result[i] + get_quality_bonus(c, game_result[i], s); + if (log_simulations) + LIBBOARDGAME_LOG("Result sco=", s, " game_res=", game_result[i], + " res=", result[i]); + } + if (m_bd.get_variant() == Variant::classic_3) + { + result[3] = result[0]; + result[4] = result[1]; + result[5] = result[2]; + } +} + +/** Evaluation function for Duo, Junior and Callisto Two-Player. */ +void State::evaluate_twocolor(array& result) +{ + LIBBOARDGAME_ASSERT(m_bd.get_nu_players() == 2); + LIBBOARDGAME_ASSERT(m_bd.get_nu_colors() == 2); + ScoreType s; + if (! m_is_symmetry_broken + && m_bd.get_nu_onboard_pieces() >= m_symmetry_min_nu_pieces) + { + if (log_simulations) + LIBBOARDGAME_LOG("Symmetry not broken"); + s = 0; + } + else + s = m_bd.get_score_twocolor(Color(0)); + Float res; + if (s > 0) + res = 1; + else if (s < 0 || (m_is_callisto && s == 0)) + res = 0; + else + res = 0.5; + if (log_simulations) + LIBBOARDGAME_LOG("Result sco=", s, " game_res=", res); + res += get_quality_bonus(Color(0), res, s); + if (m_is_callisto) + res += get_quality_bonus_attach_twocolor(); + if (log_simulations) + LIBBOARDGAME_LOG("res=", res); + result[0] = res; + result[1] = 1.f - res; +} + +Point State::find_best_starting_point(Color c) const +{ + // We use the starting point that maximizes the distance to occupied + // starting points, especially to the ones occupied by the player (their + // distance is weighted with a factor of 2). + Point best = Point::null(); + float max_distance = -1; + auto board_type = m_bd.get_board_type(); + bool is_trigon = (board_type == BoardType::trigon + || board_type == BoardType::trigon_3); + bool is_nexos = board_type == BoardType::nexos; + float ratio = (is_trigon ? 1.732f : 1); + auto& geo = m_bd.get_geometry(); + for (Point p : m_bd.get_starting_points(c)) + { + if (m_bd.is_forbidden(p, c)) + continue; + if (is_nexos) + { + // Don't use the starting segments towards the edge of the board + auto x = geo.get_x(p); + if (x <= 3 || x >= geo.get_width() - 3 - 1) + continue; + auto y = geo.get_y(p); + if (y <= 3 || y >= geo.get_height() - 3 - 1) + continue; + } + float px = static_cast(geo.get_x(p)); + float py = static_cast(geo.get_y(p)); + float d = 0; + for (Color i : Color::Range(m_nu_colors)) + for (Point pp : m_bd.get_starting_points(i)) + { + PointState s = m_bd.get_point_state(pp); + if (! s.is_empty()) + { + float ppx = static_cast(geo.get_x(pp)); + float ppy = static_cast(geo.get_y(pp)); + float dx = ppx - px; + float dy = ratio * (ppy - py); + float weight = 1; + if (s == c || s == m_bd.get_second_color(c)) + weight = 2; + d += weight * sqrt(dx * dx + dy * dy); + } + } + if (d > max_distance) + { + best = p; + max_distance = d; + } + } + return best; +} + +bool State::gen_children(Tree::NodeExpander& expander, Float root_val) +{ + if (m_nu_passes == m_nu_colors) + return true; + Color to_play = m_bd.get_to_play(); + if (m_max_piece_size == 5) + { + init_moves_without_gamma<5>(to_play); + return m_prior_knowledge.gen_children<5, 16>(m_bd, m_moves[to_play], + m_is_symmetry_broken, + expander, root_val); + } + else if (m_max_piece_size == 6) + { + init_moves_without_gamma<6>(to_play); + return m_prior_knowledge.gen_children<6, 22>(m_bd, m_moves[to_play], + m_is_symmetry_broken, + expander, root_val); + } + else + { + LIBBOARDGAME_ASSERT(m_max_piece_size == 7); + init_moves_without_gamma<7>(to_play); + return m_prior_knowledge.gen_children<7, 12>(m_bd, m_moves[to_play], + m_is_symmetry_broken, + expander, root_val); + } +} + +bool State::gen_playout_move_full(PlayerMove& mv) +{ + Color to_play = m_bd.get_to_play(); + while (true) + { + if (! m_is_move_list_initialized[to_play]) + { + if (m_max_piece_size == 5) + { + if (m_is_callisto) + init_moves_with_gamma<5, 16, true>(to_play); + else + init_moves_with_gamma<5, 16, false>(to_play); + } + else if (m_max_piece_size == 6) + init_moves_with_gamma<6, 22, false>(to_play); + else + init_moves_with_gamma<7, 12, false>(to_play); + } + else if (m_has_moves[to_play]) + { + if (m_max_piece_size == 5) + { + if (m_is_callisto) + update_moves<5, 16, true>(to_play); + else + update_moves<5, 16, false>(to_play); + } + else if (m_max_piece_size == 6) + update_moves<6, 22, false>(to_play); + else + update_moves<7, 12, false>(to_play); + } + if ((m_has_moves[to_play] = ! m_moves[to_play].empty())) + break; + if (++m_nu_passes == m_nu_colors) + return false; + if (m_check_terminate_early && m_bd.get_score_twoplayer(to_play) < 0 + && ! m_has_moves[m_bd.get_second_color(to_play)]) + { + if (log_simulations) + LIBBOARDGAME_LOG("Terminate early (no moves and neg. score)"); + return false; + } + to_play = to_play.get_next(m_nu_colors); + m_bd.set_to_play(to_play); + // Don't try to handle symmetry after pass moves + m_is_symmetry_broken = true; + } + + auto& moves = m_moves[to_play]; + LIBBOARDGAME_ASSERT(! moves.empty()); + auto total_gamma = m_cumulative_gamma[moves.size() - 1]; + if (log_simulations) + LIBBOARDGAME_LOG("Moves: ", moves.size(), ", total_gamma: ", + total_gamma); + auto begin = m_cumulative_gamma.begin(); + auto end = begin + moves.size(); + auto random = m_random.generate_float(0, total_gamma); + auto pos = lower_bound(begin, end, random); + LIBBOARDGAME_ASSERT(pos != end); + mv = PlayerMove(get_player(), + moves[static_cast(pos - begin)]); + return true; +} + +string State::get_info() const +{ + ostringstream s; + if (m_bd.get_nu_players() == 2) + { + s << "Sco: "; + m_stat_score[Color(0)].write(s, true, 1); + } + s << '\n'; + return s.str(); +} + +inline const PieceMap& State::get_is_piece_considered(Color c) const +{ + if (m_is_callisto + && m_bd.get_nu_left_piece(c, m_bd.get_one_piece()) > 1) + return m_shared_const.is_piece_considered_none; + // Use number of on-board pieces for move number to handle the case where + // there are more pieces on the board than moves (setup positions) + unsigned nu_moves = m_bd.get_nu_onboard_pieces(); + if (nu_moves >= m_shared_const.min_move_all_considered + || m_force_consider_all_pieces) + return m_shared_const.is_piece_considered_all; + return *m_shared_const.is_piece_considered[nu_moves]; +} + +/** Initializes and returns m_pieces_considered if not all pieces are + considered, otherwise m_bd.get_pieces_left(c) is returned. */ +inline const Board::PiecesLeftList& State::get_pieces_considered(Color c) +{ + auto is_piece_considered = m_is_piece_considered[c]; + auto& pieces_left = m_bd.get_pieces_left(c); + if (is_piece_considered == &m_shared_const.is_piece_considered_all + && ! m_is_callisto) + return pieces_left; + unsigned n = 0; + for (Piece piece : pieces_left) + if ((*is_piece_considered)[piece]) + m_pieces_considered.get_unchecked(n++) = piece; + m_pieces_considered.resize(n); + return m_pieces_considered; +} + +/** Basic bonus added to the result for quality-based rewards. + See also: Pepels et al.: Quality-based Rewards for Monte-Carlo Tree Search + Simulations. ECAI 2014. */ +inline Float State::get_quality_bonus(Color c, Float result, Float score) +{ + Float bonus = 0; + + // Game length + Float l = static_cast(m_bd.get_nu_moves()); + m_stat_len.add(l); + Float var = m_stat_len.get_variance(); + if (var > 0) + bonus += -0.12f * (result - 0.5f) + * sigmoid(2.f, (l - m_stat_len.get_mean()) / sqrt(var)); + + // Game score + auto& stat = m_stat_score[c]; + stat.add(score); + var = stat.get_variance(); + if (var > 0) + bonus += 0.3f * sigmoid(2.f, (score - stat.get_mean()) / sqrt(var)); + return bonus; +} + +/** Additional quality-based rewards based on number of attach points. + The number of non-forbidden attach points is another feature of a superior + final position. Only used in some two-player variants, mainly helps in + Trigon. */ +inline Float State::get_quality_bonus_attach_twocolor() +{ + LIBBOARDGAME_ASSERT(m_bd.get_nu_players() == 2); + int n = m_bd.get_attach_points(Color(0)).size() + - m_bd.get_attach_points(Color(1)).size(); + for (Point p : m_bd.get_attach_points(Color(0))) + n -= m_bd.is_forbidden(p, Color(0)); + for (Point p : m_bd.get_attach_points(Color(1))) + n += m_bd.is_forbidden(p, Color(1)); + Float attach = static_cast(n); + m_stat_attach.add(attach); + auto var = m_stat_attach.get_variance(); + if (var > 0) + return 0.1f * sigmoid(2.f, + (attach - m_stat_attach.get_mean()) / sqrt(var)); + return 0; +} + +/** Like get_quality_bonus_attach_twocolor() but for 2 colors per player. */ +inline Float State::get_quality_bonus_attach_multicolor() +{ + LIBBOARDGAME_ASSERT(m_bd.get_nu_players() == 2); + LIBBOARDGAME_ASSERT(m_bd.get_nu_colors() == 4); + int n = m_bd.get_attach_points(Color(0)).size() + + m_bd.get_attach_points(Color(2)).size() + - m_bd.get_attach_points(Color(1)).size() + - m_bd.get_attach_points(Color(3)).size(); + for (Point p : m_bd.get_attach_points(Color(0))) + n -= m_bd.is_forbidden(p, Color(0)); + for (Point p : m_bd.get_attach_points(Color(2))) + n -= m_bd.is_forbidden(p, Color(2)); + for (Point p : m_bd.get_attach_points(Color(1))) + n += m_bd.is_forbidden(p, Color(1)); + for (Point p : m_bd.get_attach_points(Color(3))) + n += m_bd.is_forbidden(p, Color(3)); + Float attach = static_cast(n); + m_stat_attach.add(attach); + auto var = m_stat_attach.get_variance(); + if (var > 0) + return 0.1f * sigmoid(2.f, + (attach - m_stat_attach.get_mean()) / sqrt(var)); + return 0; +} + +template +void State::init_moves_with_gamma(Color c) +{ + m_is_piece_considered[c] = &get_is_piece_considered(c); + m_playout_features[c].set_local(m_bd); + auto& marker = m_marker[c]; + auto& moves = m_moves[c]; + marker.clear(moves); + auto& pieces = get_pieces_considered(c); + if (m_bd.is_first_piece(c) && ! (MAX_SIZE == 5 && m_is_callisto)) + add_starting_moves(c, pieces, true, moves); + else + { + unsigned nu_moves = 0; + float total_gamma = 0; + if (MAX_SIZE == 5 && m_is_callisto) + add_one_piece_moves(c, true, total_gamma, moves, + nu_moves); + if (m_is_piece_considered[c] + != &m_shared_const.is_piece_considered_none) + for (Point p : m_bd.get_attach_points(c)) + { + if (m_bd.is_forbidden(p, c)) + continue; + add_moves(p, c, pieces, total_gamma, + moves, nu_moves); + m_moves_added_at[c][p] = true; + } + moves.resize(nu_moves); + } + m_is_move_list_initialized[c] = true; + m_nu_new_moves[c] = 0; + m_last_attach_points_end[c] = m_bd.get_attach_points(c).end(); + if (moves.empty() && + m_is_piece_considered[c] + != &m_shared_const.is_piece_considered_all) + { + m_force_consider_all_pieces = true; + init_moves_with_gamma(c); + } +} + +template +void State::init_moves_without_gamma(Color c) +{ + m_is_piece_considered[c] = &get_is_piece_considered(c); + auto& marker = m_marker[c]; + auto& moves = m_moves[c]; + marker.clear(moves); + auto& pieces = get_pieces_considered(c); + auto& is_forbidden = m_bd.is_forbidden(c); + if (m_bd.is_first_piece(c) && ! (MAX_SIZE == 5 && m_is_callisto)) + add_starting_moves(c, pieces, false, moves); + else + { + unsigned nu_moves = 0; + if (MAX_SIZE == 5 && m_is_callisto) + { + float total_gamma_dummy; + add_one_piece_moves(c, false, total_gamma_dummy, moves, + nu_moves); + } + if (m_is_piece_considered[c] + != &m_shared_const.is_piece_considered_none) + for (Point p : m_bd.get_attach_points(c)) + { + if (is_forbidden[p]) + continue; + auto adj_status = m_bd.get_adj_status(p, c); + for (Piece piece : pieces) + { + if (! has_moves(c, piece, p, adj_status)) + continue; + for (Move mv : get_moves(c, piece, p, adj_status)) + if (! marker[mv] + && check_forbidden( + is_forbidden, mv, moves, nu_moves)) + marker.set(mv); + } + m_moves_added_at[c][p] = true; + } + moves.resize(nu_moves); + } + m_is_move_list_initialized[c] = true; + m_nu_new_moves[c] = 0; + m_last_attach_points_end[c] = m_bd.get_attach_points(c).end(); + if (moves.empty() && + m_is_piece_considered[c] + != &m_shared_const.is_piece_considered_all) + { + m_force_consider_all_pieces = true; + init_moves_without_gamma(c); + } +} + +void State::play_expanded_child(Move mv) +{ + if (log_simulations) + LIBBOARDGAME_LOG("Playing expanded child"); + if (! mv.is_null()) + play_playout(mv); + else + { + ++m_nu_passes; + m_bd.set_to_play(m_bd.get_to_play().get_next(m_nu_colors)); + // Don't try to handle pass moves: a pass move either breaks symmetry + // or both players have passed and it's the end of the game and we need + // symmetry detection only as a heuristic (playouts and move value + // initialization) + m_is_symmetry_broken = true; + if (log_simulations) + LIBBOARDGAME_LOG(m_bd); + } +} + +void State::start_search() +{ + auto& bd = *m_shared_const.board; + m_bd.copy_from(bd); + m_bd.set_to_play(m_shared_const.to_play); + m_bd.take_snapshot(); + m_nu_colors = bd.get_nu_colors(); + m_is_callisto = (bd.get_piece_set() == PieceSet::callisto); + for (Color c : Color::Range(m_nu_colors)) + m_playout_features[c].init_snapshot(m_bd, c); + m_bc = &m_bd.get_board_const(); + m_max_piece_size = m_bc->get_max_piece_size(); + m_move_info_array = m_bc->get_move_info_array(); + m_move_info_ext_array = m_bc->get_move_info_ext_array(); + m_check_terminate_early = + (bd.get_nu_moves() < 10u * m_nu_colors + && m_bd.get_nu_players() == 2); + auto variant = bd.get_variant(); + m_check_symmetric_draw = + (has_central_symmetry(variant) + && ! ((m_shared_const.to_play == Color(1) + || m_shared_const.to_play == Color(3)) + && m_shared_const.avoid_symmetric_draw) + && ! check_symmetry_broken(bd)); + if (! m_check_symmetric_draw) + // Pretending that the symmetry is always broken is equivalent to + // ignoring symmetric draws + m_is_symmetry_broken = true; + if (variant == Variant::trigon_2 || variant == Variant::callisto_2) + m_symmetry_min_nu_pieces = 5; + else + { + LIBBOARDGAME_ASSERT(! m_check_symmetric_draw || variant == Variant::duo + || variant == Variant::junior); + m_symmetry_min_nu_pieces = 3; + } + + m_prior_knowledge.start_search(bd); + m_stat_len.clear(); + m_stat_attach.clear(); + for (Color c : Color::Range(m_nu_colors)) + m_stat_score[c].clear(); + + // Init gamma values + float gamma_size_factor = 1; + float gamma_nu_attach_factor = 1; + switch (bd.get_board_type()) + { + case BoardType::classic: + gamma_size_factor = 5; + break; + case BoardType::duo: + gamma_size_factor = 3; + gamma_nu_attach_factor = 1.8f; + break; + case BoardType::trigon: + case BoardType::trigon_3: // Not tuned + gamma_size_factor = 5; + break; + case BoardType::nexos: // Not tuned + gamma_size_factor = 5; + gamma_nu_attach_factor = 1.8f; + break; + case BoardType::callisto_2: + case BoardType::callisto: // Not tuned + case BoardType::callisto_3: // Not tuned + gamma_size_factor = 12; + gamma_nu_attach_factor = 1.8f; + break; + } + for (Piece::IntType i = 0; i < m_bc->get_nu_pieces(); ++i) + { + Piece piece(i); + auto score_points = m_bc->get_piece_info(piece).get_score_points(); + auto piece_nu_attach = + static_cast(m_bc->get_nu_attach_points(piece)); + LIBBOARDGAME_ASSERT(score_points >= 0); + LIBBOARDGAME_ASSERT(piece_nu_attach > 0); + m_gamma_piece[piece] = + pow(gamma_size_factor, score_points) + * pow(gamma_nu_attach_factor, piece_nu_attach - 1); + } +} + +void State::start_simulation(size_t n) +{ +#if LIBBOARDGAME_DISABLE_LOG + LIBBOARDGAME_UNUSED(n); +#endif + if (log_simulations) + LIBBOARDGAME_LOG("=================================================\n", + "Simulation ", n, "\n", + "================================================="); + m_bd.restore_snapshot(); + m_force_consider_all_pieces = false; + auto& geo = m_bd.get_geometry(); + for (Color c : Color::Range(m_nu_colors)) + { + m_has_moves[c] = true; + m_is_move_list_initialized[c] = false; + m_playout_features[c].restore_snapshot(m_bd); + m_moves_added_at[c].fill(false, geo); + } + m_nu_passes = 0; +} + +template +void State::update_moves(Color c) +{ + auto& playout_features = m_playout_features[c]; + playout_features.set_local(m_bd); + + auto& marker = m_marker[c]; + + // Find old moves that are still legal + auto& is_forbidden = m_bd.is_forbidden(c); + auto& moves = m_moves[c]; + unsigned nu_moves = 0; + float total_gamma = 0; + Piece piece; + if (m_nu_new_moves[c] == 1 && + ! m_bd.is_piece_left( + c, (piece = + get_move_info(m_last_move[c]).get_piece()))) + for (Move mv : moves) + { + auto& info = get_move_info(mv); + if (info.get_piece() == piece + || ! check_move( + mv, info, moves, nu_moves, playout_features, + total_gamma)) + marker.clear(mv); + } + else + for (Move mv : moves) + { + auto& info = get_move_info(mv); + if (! m_bd.is_piece_left(c, info.get_piece()) + || ! check_move( + mv, info, moves, nu_moves, playout_features, + total_gamma)) + marker.clear(mv); + } + + // Find new legal moves because of new pieces played by this color + auto& pieces = get_pieces_considered(c); + auto& attach_points = m_bd.get_attach_points(c); + auto begin = m_last_attach_points_end[c]; + auto end = attach_points.end(); + for (auto i = begin; i != end; ++i) + if (! is_forbidden[*i] && ! m_moves_added_at[c][*i]) + { + m_moves_added_at[c][*i] = true; + add_moves(*i, c, pieces, total_gamma, moves, + nu_moves); + } + m_nu_new_moves[c] = 0; + m_last_attach_points_end[c] = end; + + // Generate moves for pieces not considered in the last position + if (m_is_piece_considered[c] != &m_shared_const.is_piece_considered_all) + { + auto& is_piece_considered = *m_is_piece_considered[c]; + if (nu_moves == 0) + m_force_consider_all_pieces = true; + auto& is_piece_considered_new = get_is_piece_considered(c); + if (&is_piece_considered != &is_piece_considered_new) + { + Board::PiecesLeftList new_pieces; + unsigned n = 0; + for (Piece piece : m_bd.get_pieces_left(c)) + if (! is_piece_considered[piece] + && is_piece_considered_new[piece]) + new_pieces.get_unchecked(n++) = piece; + new_pieces.resize(n); + for (Point p : attach_points) + if (! is_forbidden[p]) + add_moves( + p, c, new_pieces, total_gamma, moves, nu_moves); + m_is_piece_considered[c] = &is_piece_considered_new; + } + } + moves.resize(nu_moves); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts diff --git a/src/libpentobi_mcts/State.h b/src/libpentobi_mcts/State.h new file mode 100644 index 0000000..84fd37a --- /dev/null +++ b/src/libpentobi_mcts/State.h @@ -0,0 +1,545 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/State.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_MCTS_STATE_H +#define LIBPENTOBI_MCTS_STATE_H + +#include "PlayoutFeatures.h" +#include "PriorKnowledge.h" +#include "SharedConst.h" +#include "StateUtil.h" +#include "libboardgame_mcts/LastGoodReply.h" +#include "libboardgame_mcts/PlayerMove.h" +#include "libboardgame_util/Log.h" +#include "libboardgame_util/RandomGenerator.h" +#include "libboardgame_util/Statistics.h" + +namespace libpentobi_mcts { + +using libboardgame_mcts::LastGoodReply; +using libboardgame_mcts::PlayerInt; +using libboardgame_mcts::PlayerMove; +using libboardgame_util::RandomGenerator; +using libboardgame_util::Statistics; +using libpentobi_base::BoardConst; +using libpentobi_base::MoveInfo; +using libpentobi_base::MoveInfoExt; +using libpentobi_base::Piece; +using libpentobi_base::PieceInfo; +using libpentobi_base::PieceSet; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +/** A state of a simulation. + This class contains modifiable data used in a simulation. In multi-threaded + search (not yet implemented), each thread uses its own instance of this + class. + This class incrementally keeps track of the legal moves. + The randomization in the playouts is done by assigning a heuristically + tuned gamma value to each move. The gamma value determines the probabilty + that a move is played in the playout phase. */ +class State +{ +public: + typedef libboardgame_mcts::Node + Node; + + typedef libboardgame_mcts::Tree Tree; + + typedef libboardgame_mcts::LastGoodReply + LastGoodReply; + + /** Constructor. + @param initial_variant Game variant to initialize the internal + board with (may avoid unnecessary BoardConst creation for game variant + that is never used) + @param shared_const (@ref libboardgame_doc_storesref) */ + State(Variant initial_variant, const SharedConst& shared_const); + + State& operator=(const State&) = delete; + + /** Play a move in the in-tree phase of the search. */ + void play_in_tree(Move mv); + + /** Handle end of in-tree phase. */ + void finish_in_tree(); + + /** Play a move right after expanding a node. */ + void play_expanded_child(Move mv); + + /** Finish in-tree phase without expanding a node. */ + void finish_in_tree_no_expansion(); + + /** Get current player to play. */ + PlayerInt get_player() const; + + void start_search(); + + void start_simulation(size_t n); + + bool gen_children(Tree::NodeExpander& expander, Float root_val); + + void start_playout() { } + + /** Generate a playout move. + @return @c false if end of game was reached, and no move was + generated. */ + bool gen_playout_move(const LastGoodReply& lgr, Move last, + Move second_last, PlayerMove& move); + + void evaluate_playout(array& result); + + void play_playout(Move mv); + + /** Do not update RAVE values for n'th move of the current simulation. */ + bool skip_rave(Move mv) const; + +#if LIBBOARDGAME_DEBUG + string dump() const; +#endif + + string get_info() const; + +private: + static const bool log_simulations = false; + + /** The cumulative gamma value of the moves in m_moves. */ + array m_cumulative_gamma; + + Color::IntType m_nu_passes; + + const SharedConst& m_shared_const; + + Board m_bd; + + const BoardConst* m_bc; + + Color::IntType m_nu_colors; + + BoardConst::MoveInfoArray m_move_info_array; + + BoardConst::MoveInfoExtArray m_move_info_ext_array; + + /** Incrementally updated lists of legal moves for both colors. + Only the move list for the color to play van be used in any given + position, the other color is not updated immediately after a move. */ + ColorMap m_moves; + + ColorMap*> m_is_piece_considered; + + /** The list of pieces considered in the current move if not all pieces + are considered. */ + Board::PiecesLeftList m_pieces_considered; + + PriorKnowledge m_prior_knowledge; + + /** Gamma value for a piece. */ + PieceMap m_gamma_piece; + + /** Number of moves played by a color since the last update of its move + list. */ + ColorMap m_nu_new_moves; + + /** Board::get_attach_points().end() for a color at the last update of + its move list. */ + ColorMap m_last_attach_points_end; + + /** Last move played by a color since the last update of its move list. */ + ColorMap m_last_move; + + ColorMap m_is_move_list_initialized; + + ColorMap m_has_moves; + + /** Marks moves contained in m_moves. */ + ColorMap m_marker; + + ColorMap m_playout_features; + + RandomGenerator m_random; + + /** Used in get_quality_bonus(). */ + ColorMap> m_stat_score; + + /** Used in get_quality_bonus(). */ + Statistics m_stat_len; + + /** Used in get_quality_bonus(). */ + Statistics m_stat_attach; + + bool m_check_symmetric_draw; + + bool m_check_terminate_early; + + bool m_is_symmetry_broken; + + /** Enforce all pieces to be considered for the rest of the simulation. + This applies to all colors, because it is only used if no moves were + generated because not all pieces were considered and this case is so + rare that it is not worth the cost of setting such a flag for each + color individually. */ + bool m_force_consider_all_pieces; + + bool m_is_callisto; + + /** Minimum number of pieces on board to perform a symmetry check. + 3 in Duo/Junior or 5 in Trigon because this is the earliest move number + to break the symmetry. The early playout termination that evaluates all + symmetric positions as a draw should not be used earlier because it can + cause bad move selection in very short searches if all moves are + evaluated as draw and the search is not deep enough to find that the + symmetry can be broken a few moves later. */ + unsigned m_symmetry_min_nu_pieces; + + /** Cache of m_bc->get_max_piece_size() */ + unsigned m_max_piece_size; + + /** Remember attach points that were already used for move generation. + Allows the incremental update of the move lists to skip attach points + of newly played pieces that were already attach points of previously + played pieces. */ + ColorMap> m_moves_added_at; + + + template + void add_moves(Point p, Color c, const Board::PiecesLeftList& pieces, + float& total_gamma, MoveList& moves, unsigned& nu_moves); + + template + LIBBOARDGAME_NOINLINE + void add_starting_moves(Color c, const Board::PiecesLeftList& pieces, + bool with_gamma, MoveList& moves); + + template + LIBBOARDGAME_NOINLINE + void add_one_piece_moves(Color c, bool with_gamma, float& total_gamma, + MoveList& moves, unsigned& nu_moves); + + void evaluate_multicolor(array& result); + + void evaluate_multiplayer(array& result); + + void evaluate_twocolor(array& result); + + Point find_best_starting_point(Color c) const; + + Float get_quality_bonus(Color c, Float result, Float score); + + Float get_quality_bonus_attach_twocolor(); + + Float get_quality_bonus_attach_multicolor(); + + template + const MoveInfo& get_move_info(Move mv) const; + + template + const MoveInfoExt& get_move_info_ext(Move mv) const; + + PrecompMoves::Range get_moves(Color c, Piece piece, Point p, + unsigned adj_status) const; + + bool has_moves(Color c, Piece piece, Point p, unsigned adj_status) const; + + const PieceMap& get_is_piece_considered(Color c) const; + + const Board::PiecesLeftList& get_pieces_considered(Color c); + + template + void init_moves_with_gamma(Color c); + + template + void init_moves_without_gamma(Color c); + + template + bool check_forbidden(const GridExt& is_forbidden, Move mv, + MoveList& moves, unsigned& nu_moves); + + bool check_lgr(Move mv) const; + + template + bool check_move(Move mv, const MoveInfo& info, float gamma_piece, + MoveList& moves, unsigned& nu_moves, + const PlayoutFeatures& playout_features, + float& total_gamma); + + template + bool check_move(Move mv, const MoveInfo& info, MoveList& moves, + unsigned& nu_moves, + const PlayoutFeatures& playout_features, + float& total_gamma); + + bool gen_playout_move_full(PlayerMove& mv); + + template + void update_moves(Color c); + + template + void update_playout_features(Color c, Move mv); + + template + LIBBOARDGAME_NOINLINE void update_symmetry_broken(Move mv); +}; + +/** Check if last-good-reply move is applicable. + To be faster, it doesn't check for starting moves because such moves rarely + occur in the playout phase and doesn't check if a 1-piece move is in the + center in Callisto because such moves are not generated in the search. */ +inline bool State::check_lgr(Move mv) const +{ + if (mv.is_null()) + return false; + Color c = m_bd.get_to_play(); + auto piece = m_bd.get_move_piece(mv); + if (! m_bd.is_piece_left(c, piece)) + return false; + auto points = m_bd.get_move_points(mv); + auto i = points.begin(); + auto end = points.end(); + bool has_attach_point = false; + do + { + if (m_bd.is_forbidden(*i, c)) + return false; + // Logically, we mean: + // has_attach_point = has_attach_point || is_attach_point(*i, c) + // But this generates branches, which are bad for performance in this + // tight loop (unrolled by the compiler). So we use a bitwise OR, which + // works because C++ guarantees that true/false converts to 1/0. + has_attach_point |= m_bd.is_attach_point(*i, c); + } + while (++i != end); + if (m_is_callisto) + { + Piece one_piece = m_bd.get_one_piece(); + if (piece == one_piece) + return true; + if (m_bd.get_nu_left_piece(c, one_piece) > 1 && piece != one_piece) + return false; + } + return has_attach_point; +} + +inline void State::evaluate_playout(array& result) +{ + auto nu_players = m_bd.get_nu_players(); + if (nu_players == 2) + { + if (m_nu_colors == 2) + evaluate_twocolor(result); + else + evaluate_multicolor(result); + } + else + evaluate_multiplayer(result); +} + +inline void State::finish_in_tree() +{ + if (log_simulations) + LIBBOARDGAME_LOG("Finish in-tree"); + if (m_check_symmetric_draw) + m_is_symmetry_broken = check_symmetry_broken(m_bd); +} + +inline bool State::gen_playout_move(const LastGoodReply& lgr, Move last, + Move second_last, PlayerMove& mv) +{ + if (m_nu_passes == m_nu_colors) + return false; + if (! m_is_symmetry_broken + && m_bd.get_nu_onboard_pieces() >= m_symmetry_min_nu_pieces) + { + // See also the comment in evaluate_playout() + if (log_simulations) + LIBBOARDGAME_LOG("Terminate playout. Symmetry not broken."); + return false; + } + PlayerInt player = get_player(); + Move lgr2 = lgr.get_lgr2(player, last, second_last); + if (check_lgr(lgr2)) + { + if (log_simulations) + LIBBOARDGAME_LOG("Playing last good reply 2"); + mv = PlayerMove(player, lgr2); + return true; + } + Move lgr1 = lgr.get_lgr1(player, last); + if (check_lgr(lgr1)) + { + if (log_simulations) + LIBBOARDGAME_LOG("Playing last good reply 1"); + mv = PlayerMove(player, lgr1); + return true; + } + return gen_playout_move_full(mv); +} + +template +inline const MoveInfo& State::get_move_info(Move mv) const +{ + LIBBOARDGAME_ASSERT(mv.to_int() < m_bc->get_nu_moves()); + return BoardConst::get_move_info(mv, m_move_info_array); +} + +template +inline const MoveInfoExt& State::get_move_info_ext( + Move mv) const +{ + LIBBOARDGAME_ASSERT(mv.to_int() < m_bc->get_nu_moves()); + return BoardConst::get_move_info_ext( + mv, m_move_info_ext_array); +} + +inline PrecompMoves::Range State::get_moves(Color c, Piece piece, Point p, + unsigned adj_status) const +{ + return m_shared_const.precomp_moves[c].get_moves(piece, p, adj_status); +} + +inline PlayerInt State::get_player() const +{ + unsigned player = m_bd.get_to_play().to_int(); + if ( m_bd.get_variant() == Variant::classic_3 && player == 3) + player += m_bd.get_alt_player(); + return static_cast(player); +} + +inline bool State::has_moves(Color c, Piece piece, Point p, + unsigned adj_status) const +{ + return m_shared_const.precomp_moves[c].has_moves(piece, p, adj_status); +} + +inline void State::play_in_tree(Move mv) +{ + Color to_play = m_bd.get_to_play(); + if (! mv.is_null()) + { + LIBBOARDGAME_ASSERT(m_bd.is_legal(to_play, mv)); + m_nu_passes = 0; + if (m_max_piece_size == 5) + { + m_bd.play<5, 16>(to_play, mv); + update_playout_features<5, 16>(to_play, mv); + } + else if (m_max_piece_size == 6) + { + m_bd.play<6, 22>(to_play, mv); + update_playout_features<6, 22>(to_play, mv); + } + else + { + m_bd.play<7, 12>(to_play, mv); + update_playout_features<7, 12>(to_play, mv); + } + } + else + { + ++m_nu_passes; + m_bd.set_to_play(to_play.get_next(m_nu_colors)); + } + if (log_simulations) + LIBBOARDGAME_LOG(m_bd); +} + +inline void State::play_playout(Move mv) +{ + auto to_play = m_bd.get_to_play(); + LIBBOARDGAME_ASSERT(m_bd.is_legal(to_play, mv)); + if (m_max_piece_size == 5) + { + m_bd.play<5, 16>(to_play, mv); + update_playout_features<5, 16>(to_play, mv); + if (! m_is_symmetry_broken) + update_symmetry_broken<5>(mv); + } + else if (m_max_piece_size == 6) + { + m_bd.play<6, 22>(to_play, mv); + update_playout_features<6, 22>(to_play, mv); + if (! m_is_symmetry_broken) + update_symmetry_broken<6>(mv); + } + else + { + m_bd.play<7, 12>(to_play, mv); + update_playout_features<7, 12>(to_play, mv); + // No game variant with piece size 7 uses m_is_symmetry_broken + } + ++m_nu_new_moves[to_play]; + m_last_move[to_play] = mv; + m_nu_passes = 0; + if (log_simulations) + LIBBOARDGAME_LOG(m_bd); +} + +inline bool State::skip_rave(Move mv) const +{ + LIBBOARDGAME_UNUSED(mv); + return false; +} + +template +inline void State::update_playout_features(Color c, Move mv) +{ + auto& info = get_move_info(mv); + for (Color i : Color::Range(m_nu_colors)) + m_playout_features[i].set_forbidden(info); + m_playout_features[c].set_forbidden( + get_move_info_ext(mv)); +} + +template +void State::update_symmetry_broken(Move mv) +{ + Color to_play = m_bd.get_to_play(); + Color second_color = m_bd.get_second_color(to_play); + auto& symmetric_points = m_bc->get_symmetrc_points(); + auto& info = get_move_info(mv); + auto i = info.begin(); + auto end = info.end(); + if (to_play == Color(0) || to_play == Color(2)) + { + // First player to play: Check that all symmetric points of the last + // move of the second player are occupied by the first player + do + { + Point symm_p = symmetric_points[*i]; + if (m_bd.get_point_state(symm_p) != second_color) + { + m_is_symmetry_broken = true; + return; + } + } + while (++i != end); + } + else + { + // Second player to play: Check that all symmetric points of the last + // move of the first player are empty (i.e. the second player can play + // there to preserve the symmetry) + do + { + Point symm_p = symmetric_points[*i]; + if (! m_bd.get_point_state(symm_p).is_empty()) + { + m_is_symmetry_broken = true; + return; + } + } + while (++i != end); + } +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts + +#endif // LIBPENTOBI_MCTS_STATE_H diff --git a/src/libpentobi_mcts/StateUtil.cpp b/src/libpentobi_mcts/StateUtil.cpp new file mode 100644 index 0000000..c85c2f5 --- /dev/null +++ b/src/libpentobi_mcts/StateUtil.cpp @@ -0,0 +1,100 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/StateUtil.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "StateUtil.h" + +namespace libpentobi_mcts { + +using libpentobi_base::Color; +using libpentobi_base::ColorMove; +using libpentobi_base::Geometry; +using libpentobi_base::Point; +using libpentobi_base::PointState; + +//----------------------------------------------------------------------------- + +namespace { + +array symmetric_state = + { Color(1), Color(0), Color(3), Color(2) }; + +} // namespace + +//----------------------------------------------------------------------------- + +bool check_symmetry_broken(const Board& bd) +{ + LIBBOARDGAME_ASSERT(has_central_symmetry(bd.get_variant())); + auto& symmetric_points = bd.get_board_const().get_symmetrc_points(); + Color to_play = bd.get_to_play(); + auto& geo = bd.get_geometry(); + // No need to iterator over the whole board when checking symmetry (this + // makes the assumption that the symmetric points of the points in the + // first half of the integer range are in the second half). + Geometry::Iterator begin = geo.begin(); + LIBBOARDGAME_ASSERT(geo.get_range() % 2 == 0); + Geometry::Iterator end(static_cast(geo.get_range() / 2)); +#if LIBBOARDGAME_DEBUG + for (auto p = begin; p != end; ++p) + LIBBOARDGAME_ASSERT(symmetric_points[*p].to_int() >= (*end).to_int()); +#endif + if (to_play == Color(0) || to_play == Color(2)) + { + // First player to play: the symmetry is broken if the position is + // not symmetric. + for (auto p = begin; p != end; ++p) + { + PointState s1 = bd.get_point_state(*p); + if (! s1.is_empty()) + { + Point symm_p = symmetric_points[*p]; + PointState s2 = bd.get_point_state(symm_p); + if (s2 != symmetric_state[s1.to_int()]) + return true; + } + } + } + else + { + // Second player to play: the symmetry is broken if the second player + // cannot copy the first player's last move to make the position + // symmetric again. + unsigned nu_moves = bd.get_nu_moves(); + if (nu_moves == 0) + // Don't try to handle the case if the second player has to play as + // first move (e.g. in setup positions) + return true; + Color previous_color = bd.get_previous(to_play); + ColorMove last_mv = bd.get_move(nu_moves - 1); + if (last_mv.color != previous_color) + // Don't try to handle non-alternating moves in board history + return true; + auto points = bd.get_move_points(last_mv.move); + for (Point p : points) + if (! bd.get_point_state(symmetric_points[p]).is_empty()) + return true; + for (auto p = begin; p != end; ++p) + { + PointState s1 = bd.get_point_state(*p); + if (! s1.is_empty()) + { + PointState s2 = bd.get_point_state(symmetric_points[*p]); + if (s2 != symmetric_state[s1.to_int()] + && ! points.contains(*p)) + return true; + } + } + } + return false; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts diff --git a/src/libpentobi_mcts/StateUtil.h b/src/libpentobi_mcts/StateUtil.h new file mode 100644 index 0000000..9431948 --- /dev/null +++ b/src/libpentobi_mcts/StateUtil.h @@ -0,0 +1,25 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/StateUtil.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_MCTS_STATE_UTIL_H +#define LIBPENTOBI_MCTS_STATE_UTIL_H + +#include "libpentobi_base/Board.h" + +namespace libpentobi_mcts { + +using namespace std; +using libpentobi_base::Board; + +//----------------------------------------------------------------------------- + +bool check_symmetry_broken(const Board& bd); + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts + +#endif // LIBPENTOBI_MCTS_STATE_UTIL_H diff --git a/src/libpentobi_mcts/Util.cpp b/src/libpentobi_mcts/Util.cpp new file mode 100644 index 0000000..0430c75 --- /dev/null +++ b/src/libpentobi_mcts/Util.cpp @@ -0,0 +1,118 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/Util.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Util.h" + +#include +#include "libboardgame_sgf/Writer.h" +#include "libboardgame_util/Log.h" +#include "libpentobi_base/BoardUtil.h" +#include "libpentobi_base/PentobiSgfUtil.h" + +namespace libpentobi_mcts { +namespace util { + +using libboardgame_mcts::Node; +using libboardgame_mcts::Tree; +using libboardgame_sgf::Writer; +using libpentobi_base::boardutil::write_setup; +using libpentobi_base::sgf_util::get_color_id; + +//----------------------------------------------------------------------------- + +namespace { + +void dump_tree_recurse(Writer& writer, Variant variant, + const Search::Tree& tree, const Search::Node& node, + Color to_play) +{ + ostringstream comment; + comment << "Visits: " << node.get_visit_count() + << "\nVal: " << node.get_value() + << "\nCnt: " << node.get_value_count(); + writer.write_property("C", comment.str()); + writer.end_node(); + Color next_to_play = to_play.get_next(get_nu_colors(variant)); + vector children; + for (auto& i : tree.get_children(node)) + children.push_back(&i); + sort(children.begin(), children.end(), compare_node); + for (const auto i : children) + { + writer.begin_tree(); + writer.begin_node(); + auto mv = i->get_move(); + if (! mv.is_null()) + { + auto& board_const = BoardConst::get(variant); + auto id = get_color_id(variant, to_play); + if (! mv.is_null()) + writer.write_property(id, board_const.to_string(mv, false)); + } + dump_tree_recurse(writer, variant, tree, *i, next_to_play); + writer.end_tree(); + } +} + +} // namespace + +//----------------------------------------------------------------------------- + +bool compare_node(const Search::Node* n1, const Search::Node* n2) +{ + Float count1 = n1->get_visit_count(); + Float count2 = n2->get_visit_count(); + if (count1 != count2) + return count1 > count2; + return n1->get_value() > n2->get_value(); +} + +void dump_tree(ostream& out, const Search& search) +{ + Variant variant; + Setup setup; + search.get_root_position(variant, setup); + Writer writer(out); + writer.begin_tree(); + writer.begin_node(); + writer.write_property("GM", to_string(variant)); + write_setup(writer, variant, setup); + writer.write_property("PL", get_color_id(variant, setup.to_play)); + auto& tree = search.get_tree(); + dump_tree_recurse(writer, variant, tree, tree.get_root(), setup.to_play); + writer.end_tree(); +} + +unsigned get_nu_threads() +{ + unsigned nu_threads = thread::hardware_concurrency(); + if (nu_threads == 0) + { + LIBBOARDGAME_LOG("Could not determine the number of hardware threads"); + nu_threads = 1; + } + // The lock-free search probably scales up to 16-32 threads, but we + // haven't tested more than 4 threads, we still use single precision + // float for LIBBOARDGAME_MCTS_FLOAT_TYPE (which limits the maximum number + // of simulations per search) and CPUs with more than 4 cores are + // currently not very common anyway. Also, the loss of playing strength + // of a multi-threaded search with the same count as a single-threaded + // search will become larger with many threads, so there would need to be + // a correction factor in the number of simulations per level to take this + // into account. + if (nu_threads > 4) + nu_threads = 4; + return nu_threads; +} + +//----------------------------------------------------------------------------- + +} // namespace util +} // namespace libpentobi_mcts diff --git a/src/libpentobi_mcts/Util.h b/src/libpentobi_mcts/Util.h new file mode 100644 index 0000000..eb96978 --- /dev/null +++ b/src/libpentobi_mcts/Util.h @@ -0,0 +1,35 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/Util.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_MCTS_UTIL_H +#define LIBPENTOBI_MCTS_UTIL_H + +#include "Search.h" + +namespace libpentobi_mcts { +namespace util { + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Comparison function for sorting children of a node by count. + Prefers nodes with higher counts. Uses the node value as a tie breaker. */ +bool compare_node(const Search::Node* n1, const Search::Node* n2); + +/** Dump the search tree in SGF format. */ +void dump_tree(ostream& out, const Search& search); + +/** Suggest how many threads to use in the search depending on the current + system. */ +unsigned get_nu_threads(); + +//----------------------------------------------------------------------------- + +} // namespace util +} // namespace libpentobi_mcts + +#endif // LIBPENTOBI_MCTS_UTIL_H diff --git a/src/libpentobi_thumbnail/CMakeLists.txt b/src/libpentobi_thumbnail/CMakeLists.txt new file mode 100644 index 0000000..77acc34 --- /dev/null +++ b/src/libpentobi_thumbnail/CMakeLists.txt @@ -0,0 +1,6 @@ +add_library(pentobi_thumbnail STATIC + CreateThumbnail.h + CreateThumbnail.cpp +) + +target_link_libraries(pentobi_thumbnail Qt5::Widgets) diff --git a/src/libpentobi_thumbnail/CreateThumbnail.cpp b/src/libpentobi_thumbnail/CreateThumbnail.cpp new file mode 100644 index 0000000..3aef69b --- /dev/null +++ b/src/libpentobi_thumbnail/CreateThumbnail.cpp @@ -0,0 +1,157 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_thumbnail/CreateThumbnail.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "CreateThumbnail.h" + +#include +#include "libboardgame_sgf/TreeReader.h" +#include "libboardgame_util/StringUtil.h" +#include "libpentobi_base/NodeUtil.h" +#include "libpentobi_gui/BoardPainter.h" + +using namespace std; +using libboardgame_sgf::SgfNode; +using libboardgame_sgf::TreeReader; +using libboardgame_util::split; +using libboardgame_util::trim; +using libpentobi_base::get_board_type; +using libpentobi_base::Geometry; +using libpentobi_base::Grid; +using libpentobi_base::PieceSet; +using libpentobi_base::PointState; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +namespace { + +/** Helper function for getFinalPosition() */ +void handleSetup(const char* id, Color c, const SgfNode& node, + const Geometry& geo, Grid& pointState, + Grid& pieceId, unsigned& currentPieceId) +{ + vector values = node.get_multi_property(id); + for (const string& s : values) + { + if (trim(s).empty()) + continue; + vector v = split(s, ','); + ++currentPieceId; + for (const string& p_str : v) + { + Point p; + if (geo.from_string(p_str, p)) + { + pointState[p] = PointState(c); + pieceId[p] = currentPieceId; + } + } + } +} + +/** Helper function for getFinalPosition() */ +void handleSetupEmpty(const SgfNode& node, const Geometry& geo, + Grid& pointState, Grid& pieceId) +{ + vector values = node.get_multi_property("AE"); + for (const auto& s : values) + { + if (trim(s).empty()) + continue; + vector v = split(s, ','); + for (const auto& p_str : v) + { + Point p; + if (geo.from_string(p_str, p)) + { + pointState[p] = PointState::empty(); + pieceId[p] = 0; + } + } + } +} + +/** Get the board state of the final position of the main variation. + Avoids constructing an instance of a Tree or Game, which would do a costly + initialization of BoardConst and slow down the thumbnailer + unnecessarily. */ +bool getFinalPosition(const SgfNode& root, Variant& variant, + const Geometry*& geo, Grid& pointState, + Grid& pieceId) +{ + if (! parse_variant(root.get_property("GM", ""), variant)) + return false; + geo = &get_geometry(variant); + pointState.fill(PointState::empty(), *geo); + auto pieceSet = get_piece_set(variant); + if (pieceSet == PieceSet::nexos || pieceSet == PieceSet::callisto) + pieceId.fill(0, *geo); + auto node = &root; + unsigned id = 0; + while (node) + { + if (libpentobi_base::node_util::has_setup(*node)) + { + handleSetup("AB", Color(0), *node, *geo, pointState, pieceId, id); + handleSetup("AW", Color(1), *node, *geo, pointState, pieceId, id); + handleSetup("A1", Color(0), *node, *geo, pointState, pieceId, id); + handleSetup("A2", Color(1), *node, *geo, pointState, pieceId, id); + handleSetup("A3", Color(2), *node, *geo, pointState, pieceId, id); + handleSetup("A4", Color(3), *node, *geo, pointState, pieceId, id); + handleSetupEmpty(*node, *geo, pointState, pieceId); + if (node == &root) + // If the file starts with a setup (e.g. a puzzle), we use this + // position for the thumbnail. + break; + } + Color c; + MovePoints points; + if (libpentobi_base::node_util::get_move(*node, variant, c, points)) + { + ++id; + for (Point p : points) + { + pointState[p] = PointState(c); + pieceId[p] = id; + } + } + node = node->get_first_child_or_null(); + } + return true; +} + +} // namespace + +//----------------------------------------------------------------------------- + +bool createThumbnail(const QString& path, int width, int height, + QImage& image) +{ + TreeReader reader; + reader.set_read_only_main_variation(true); + reader.read(path.toLocal8Bit().constData()); + auto variant = + Variant::classic; // Initialize to avoid compiler warning + const Geometry* geo; + Grid pointState; + Grid pieceId; + if (! getFinalPosition(reader.get_tree(), variant, geo, pointState, + pieceId)) + { + cerr << "Not a valid Blokus SGF file\n"; + return false; + } + QPainter painter; + if (! painter.begin(&image)) + return false; + BoardPainter boardPainter; + boardPainter.paintEmptyBoard(painter, width, height, variant, *geo); + boardPainter.paintPieces(painter, pointState, pieceId); + painter.end(); + return true; +} + +//----------------------------------------------------------------------------- diff --git a/src/libpentobi_thumbnail/CreateThumbnail.h b/src/libpentobi_thumbnail/CreateThumbnail.h new file mode 100644 index 0000000..f2b1e50 --- /dev/null +++ b/src/libpentobi_thumbnail/CreateThumbnail.h @@ -0,0 +1,20 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_thumbnail/CreateThumbnail.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef LIBPENTOBI_THUMBNAIL_CREATE_THUMBNAIL_H +#define LIBPENTOBI_THUMBNAIL_CREATE_THUMBNAIL_H + +class QImage; +class QString; + +//----------------------------------------------------------------------------- + +bool createThumbnail(const QString& path, int width, int height, + QImage& image); + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_THUMBNAIL_CREATE_THUMBNAIL_H diff --git a/src/pentobi/AnalyzeGameWidget.cpp b/src/pentobi/AnalyzeGameWidget.cpp new file mode 100644 index 0000000..0e762fb --- /dev/null +++ b/src/pentobi/AnalyzeGameWidget.cpp @@ -0,0 +1,231 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/AnalyzeGameWidget.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "AnalyzeGameWidget.h" + +#include +#include +#include +#include +#include +#include +#include "Util.h" +#include "libboardgame_sgf/SgfUtil.h" +#include "libboardgame_util/Abort.h" +#include "libpentobi_gui/Util.h" + +using libboardgame_sgf::util::find_root; +using libboardgame_sgf::util::is_main_variation; +using libboardgame_util::set_abort; +using libboardgame_util::ArrayList; +using libpentobi_base::Board; +using libpentobi_base::PentobiTree; + +//----------------------------------------------------------------------------- + +AnalyzeGameWidget::AnalyzeGameWidget(QWidget* parent) + : QWidget(parent) +{ + setMinimumSize(240, 120); + m_isInitialized = false; + m_currentPosition = -1; +} + +void AnalyzeGameWidget::cancel() +{ + if (! m_isRunning) + return; + set_abort(); + m_future.waitForFinished(); +} + +void AnalyzeGameWidget::initSize() +{ + m_borderX = width() / 50; + m_borderY = height() / 20; + m_maxX = width() - 2 * m_borderX; + m_dX = qreal(m_maxX) / Board::max_game_moves; + m_maxY = height() - 2 * m_borderY; +} + +void AnalyzeGameWidget::mousePressEvent(QMouseEvent* event) +{ + if (! m_isInitialized && m_isRunning) + return; + unsigned moveNumber = + static_cast((event->x() - m_borderX) / m_dX); + if (moveNumber >= m_analyzeGame.get_nu_moves()) + return; + vector moves; + for (unsigned i = 0; i < moveNumber; ++i) + moves.push_back(m_analyzeGame.get_move(i)); + emit gotoPosition(m_analyzeGame.get_variant(), moves); +} + +void AnalyzeGameWidget::paintEvent(QPaintEvent*) +{ + if (! m_isInitialized) + return; + QPainter painter(this); + QFont font; + font.setStyleStrategy(QFont::PreferOutline); + // QFont::setPixelSize(0) prints a warning even if it works and the docs + // of Qt 5.3 don't forbid it (unlike QFont::setPointSize(0)). + font.setPixelSize(max(1, static_cast(0.06 * height()))); + QFontMetrics metrics(font); + painter.translate(m_borderX, m_borderY); + painter.setPen(Qt::NoPen); + painter.setBrush(QColor(240, 240, 240)); + painter.drawRect(0, 0, m_maxX, m_maxY); + unsigned nu_moves = m_analyzeGame.get_nu_moves(); + if (m_currentPosition >= 0 + && static_cast(m_currentPosition) < nu_moves) + { + QPen pen(QColor(96, 96, 96)); + pen.setStyle(Qt::DotLine); + painter.setPen(pen); + int x = static_cast(m_currentPosition * m_dX + 0.5 * m_dX); + painter.drawLine(x, 0, x, m_maxY); + } + painter.setPen(QColor(32, 32, 32)); + painter.drawLine(0, 0, m_maxX, 0); + painter.drawLine(0, m_maxY, m_maxX, m_maxY); + painter.setRenderHint(QPainter::Antialiasing, true); + QString labelWin = tr("Win"); + QRect boundingRectWin = metrics.boundingRect(labelWin); + painter.drawText(QRect(0, 0, boundingRectWin.width(), + boundingRectWin.height()), + Qt::AlignLeft | Qt::AlignTop | Qt::TextDontClip, + labelWin); + QString labelLoss = tr("Loss"); + QRect boundingRectLoss = metrics.boundingRect(labelLoss); + painter.drawText(QRect(0, m_maxY - boundingRectLoss.height(), + boundingRectLoss.width(), boundingRectLoss.height()), + Qt::AlignLeft | Qt::AlignBottom | Qt::TextDontClip, + labelLoss); + painter.setRenderHint(QPainter::Antialiasing, false); + painter.setPen(QColor(128, 128, 128)); + painter.drawLine(0, m_maxY / 2, m_maxX, m_maxY / 2); + painter.setRenderHint(QPainter::Antialiasing, true); + for (unsigned i = 0; i < nu_moves; ++i) + { + double value = m_analyzeGame.get_value(i); + // Values can be outside [0..1] due to score/length bonuses + if (value < 0) + value = 0; + else if (value > 1) + value = 1; + auto color = Util::getPaintColor(m_analyzeGame.get_variant(), + m_analyzeGame.get_move(i).color); + painter.setPen(Qt::NoPen); + painter.setBrush(color); + painter.drawEllipse(QPointF((i + 0.5) * m_dX, (1 - value) * m_maxY), + 0.5 * m_dX, 0.5 * m_dX); + } +} + +void AnalyzeGameWidget::resizeEvent(QResizeEvent*) +{ + if (! m_isInitialized) + return; + initSize(); +} + +void AnalyzeGameWidget::setCurrentPosition(const Game& game, + const SgfNode& node) +{ + update(); + m_currentPosition = -1; + if (is_main_variation(node)) + { + ArrayList moves; + auto& tree = game.get_tree(); + auto current = &find_root(node); + while (current) + { + auto mv = tree.get_move(*current); + if (! mv.is_null() && moves.size() < Board::max_game_moves) + moves.push_back(mv); + if (current == &node) + break; + current = current->get_first_child_or_null(); + } + if (moves.size() <= m_analyzeGame.get_nu_moves()) + { + for (unsigned i = 0; i < moves.size(); ++i) + if (moves[i] != m_analyzeGame.get_move(i)) + return; + m_currentPosition = moves.size(); + } + } +} + +void AnalyzeGameWidget::showProgress(int progress) +{ + // m_progressDialog might already be closed if cancel was pressed and + // setValue makes it visible again (only with some Qt versions/platforms?) + if (m_progressDialog->isVisible()) + m_progressDialog->setValue(progress); + // Repaint the window with the current status of the analysis + update(); +} + +QSize AnalyzeGameWidget::sizeHint() const +{ + auto geo = QApplication::desktop()->screenGeometry(); + return QSize(geo.width() / 2, geo.height() / 3); +} + +void AnalyzeGameWidget::start(const Game& game, Search& search, + size_t nuSimulations) +{ + m_isInitialized = true; + m_game = &game; + m_search = &search; + m_nuSimulations = nuSimulations; + initSize(); + if (! m_progressDialog) + { + m_progressDialog = new QProgressDialog(this); + m_progressDialog->setWindowModality(Qt::WindowModal); + m_progressDialog->setWindowFlags(m_progressDialog->windowFlags() + & ~Qt::WindowContextHelpButtonHint); + m_progressDialog->setLabel(new QLabel(tr("Running game analysis..."), + this)); + Util::setNoTitle(*m_progressDialog); + m_progressDialog->setMinimumDuration(0); + connect(m_progressDialog, SIGNAL(canceled()), SLOT(cancel())); + } + m_progressDialog->show(); + m_isRunning = true; + m_future = QtConcurrent::run(this, &AnalyzeGameWidget::threadFunction); +} + +void AnalyzeGameWidget::threadFunction() +{ + // This function and the progress callback are not called from the GUI + // thread. So we need to invoke showProgress() with invokeMethod(). + auto progressCallback = + [&](unsigned movesAnalyzed, unsigned totalMoves) + { + if (totalMoves == 0) + return; + int progress = 100 * movesAnalyzed / totalMoves; + QMetaObject::invokeMethod(this, "showProgress", + Qt::BlockingQueuedConnection, + Q_ARG(int, progress)); + }; + m_analyzeGame.run(*m_game, *m_search, m_nuSimulations, progressCallback); + QMetaObject::invokeMethod(m_progressDialog, "hide", Qt::QueuedConnection); + m_isRunning = false; + emit finished(); +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/AnalyzeGameWidget.h b/src/pentobi/AnalyzeGameWidget.h new file mode 100644 index 0000000..a549a26 --- /dev/null +++ b/src/pentobi/AnalyzeGameWidget.h @@ -0,0 +1,117 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/AnalyzeGameWidget.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_ANALYZE_GAME_WIDGET_H +#define PENTOBI_ANALYZE_GAME_WIDGET_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include "libpentobi_mcts/AnalyzeGame.h" + +class QProgressDialog; + +using namespace std; +using libboardgame_sgf::SgfNode; +using libpentobi_base::ColorMove; +using libpentobi_base::Game; +using libpentobi_base::Variant; +using libpentobi_mcts::AnalyzeGame; +using libpentobi_mcts::Search; + +//----------------------------------------------------------------------------- + +class AnalyzeGameWidget + : public QWidget +{ + Q_OBJECT + +public slots: + /** Cancel a running analysis. + The function waits for the analysis to finish. The finished() signal + will still be invoked. */ + void cancel(); + +public: + explicit AnalyzeGameWidget(QWidget* parent); + + /** Start an analysis. + This function will return after the analysis has started but the + window will be protected by a modal cancelable progress dialog. + Don't modify the game or use the search from a different thread until + the signal finished() was emitted. This will walk through every game + position in the main variation and use the search to evaluate + positions. During the analysis, the parent window is protected with a + modal progress dialog. */ + void start(const Game& game, Search& search, size_t nuSimulations); + + /** Mark the current position. + Will clear the current position if the target node is not in the + main variation or does not correspond to a move in the move + sequence when the analysis was done. */ + void setCurrentPosition(const Game& game, const SgfNode& node); + + QSize sizeHint() const override; + +signals: + /** Tells that the analysis has finished. */ + void finished(); + + void gotoPosition(Variant variant, const vector& moves); + +protected: + void mousePressEvent(QMouseEvent* event) override; + + void paintEvent(QPaintEvent* event) override; + + void resizeEvent(QResizeEvent* event) override; + +private slots: + void showProgress(int progress); + +private: + bool m_isInitialized; + + bool m_isRunning; + + const Game* m_game; + + Search* m_search; + + size_t m_nuSimulations; + + AnalyzeGame m_analyzeGame; + + QProgressDialog* m_progressDialog = nullptr; + + QFuture m_future; + + int m_borderX; + + int m_borderY; + + qreal m_dX; + + int m_maxX; + + int m_maxY; + + /** Current position that will be marked or -1 if no position is marked. */ + int m_currentPosition; + + void initSize(); + + void threadFunction(); +}; + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_ANALYZE_GAME_WIDGET_H diff --git a/src/pentobi/AnalyzeGameWindow.cpp b/src/pentobi/AnalyzeGameWindow.cpp new file mode 100644 index 0000000..5f4a238 --- /dev/null +++ b/src/pentobi/AnalyzeGameWindow.cpp @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/AnalyzeGameWindow.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "AnalyzeGameWindow.h" + +#include +#include + +//----------------------------------------------------------------------------- + +AnalyzeGameWindow::AnalyzeGameWindow(QWidget* parent) + : QDialog(parent) +{ + setWindowTitle(tr("Game Analysis")); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + auto layout = new QVBoxLayout; + setLayout(layout); + analyzeGameWidget = new AnalyzeGameWidget(this); + layout->addWidget(analyzeGameWidget); + auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Close); + layout->addWidget(buttonBox); + connect(buttonBox, SIGNAL(rejected()), SLOT(reject())); + buttonBox->setFocus(); +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/AnalyzeGameWindow.h b/src/pentobi/AnalyzeGameWindow.h new file mode 100644 index 0000000..d546fca --- /dev/null +++ b/src/pentobi/AnalyzeGameWindow.h @@ -0,0 +1,36 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/AnalyzeGameWindow.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_ANALYZE_GAME_WINDOW_H +#define PENTOBI_ANALYZE_GAME_WINDOW_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include "AnalyzeGameWidget.h" + +using namespace std; + +//----------------------------------------------------------------------------- + +class AnalyzeGameWindow final + : public QDialog +{ + Q_OBJECT + +public: + AnalyzeGameWidget* analyzeGameWidget; + + + explicit AnalyzeGameWindow(QWidget* parent); +}; + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_ANALYZE_GAME_WINDOW_H diff --git a/src/pentobi/AnalyzeSpeedDialog.cpp b/src/pentobi/AnalyzeSpeedDialog.cpp new file mode 100644 index 0000000..3251b68 --- /dev/null +++ b/src/pentobi/AnalyzeSpeedDialog.cpp @@ -0,0 +1,37 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/AnalyzeSpeedDialog.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "AnalyzeSpeedDialog.h" + +//----------------------------------------------------------------------------- + +AnalyzeSpeedDialog::AnalyzeSpeedDialog(QWidget* parent, const QString& title) + : QInputDialog(parent) +{ + m_items << tr("Fast") << tr("Normal") << tr("Slow"); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + setWindowTitle(title); + setLabelText(tr("Analysis speed:")); + setInputMode(QInputDialog::TextInput); + setComboBoxItems(m_items); + setComboBoxEditable(false); +} + +AnalyzeSpeedDialog::~AnalyzeSpeedDialog() +{ +} + +void AnalyzeSpeedDialog::accept() +{ + m_speedValue = m_items.indexOf(textValue()); + QDialog::accept(); +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/AnalyzeSpeedDialog.h b/src/pentobi/AnalyzeSpeedDialog.h new file mode 100644 index 0000000..5bd73b2 --- /dev/null +++ b/src/pentobi/AnalyzeSpeedDialog.h @@ -0,0 +1,44 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/AnalyzeSpeedDialog.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_ANALYZE_SPEED_DIALOG_H +#define PENTOBI_ANALYZE_SPEED_DIALOG_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +//----------------------------------------------------------------------------- + +class AnalyzeSpeedDialog final + : public QInputDialog +{ + Q_OBJECT + +public: + AnalyzeSpeedDialog(QWidget* parent, const QString& title); + + ~AnalyzeSpeedDialog(); + + /** Get return value if dialog was accepted. + 0 = fast, 1 = normal, 2 = slow */ + int getSpeedValue() { return m_speedValue; } + +public slots: + void accept() override; + +private: + int m_speedValue = 0; + + QStringList m_items; +}; + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_ANALYZE_SPEED_DIALOG_H diff --git a/src/pentobi/Application.cpp b/src/pentobi/Application.cpp new file mode 100644 index 0000000..176adf4 --- /dev/null +++ b/src/pentobi/Application.cpp @@ -0,0 +1,35 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/Application.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Application.h" + +#include "ShowMessage.h" +#include "libboardgame_sys/Compiler.h" + +using namespace std; +using libboardgame_sys::get_type_name; + +//----------------------------------------------------------------------------- + +bool Application::notify(QObject* receiver, QEvent* event) +{ + try + { + return QApplication::notify(receiver, event); + } + catch (const exception& e) + { + string detailedText = get_type_name(e) + ": " + e.what(); + showFatal(QString::fromLocal8Bit(detailedText.c_str())); + } + return false; +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/Application.h b/src/pentobi/Application.h new file mode 100644 index 0000000..8e497fe --- /dev/null +++ b/src/pentobi/Application.h @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/Application.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_APPLICATION_H +#define PENTOBI_APPLICATION_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +//----------------------------------------------------------------------------- + +class Application + : public QApplication +{ + Q_OBJECT + +public: + using QApplication::QApplication; + + /** Reimplemented from QApplication::notify(). + Catches exceptions and shows an error message. */ + bool notify(QObject* receiver, QEvent* event) override; +}; + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_APPLICATION_H diff --git a/src/pentobi/CMakeLists.txt b/src/pentobi/CMakeLists.txt new file mode 100644 index 0000000..ecae6de --- /dev/null +++ b/src/pentobi/CMakeLists.txt @@ -0,0 +1,142 @@ +set(CMAKE_AUTOMOC TRUE) + +set(pentobi_SRCS + AnalyzeGameWidget.h + AnalyzeGameWidget.cpp + AnalyzeGameWindow.h + AnalyzeGameWindow.cpp + AnalyzeSpeedDialog.h + AnalyzeSpeedDialog.cpp + Application.h + Application.cpp + Main.cpp + MainWindow.h + MainWindow.cpp + RatedGamesList.h + RatedGamesList.cpp + RatingDialog.h + RatingDialog.cpp + RatingGraph.h + RatingGraph.cpp + RatingHistory.h + RatingHistory.cpp + ShowMessage.h + ShowMessage.cpp + Util.h + Util.cpp + pentobi.rc +) + +set(pentobi_ICNS + pentobi.png + pentobi-16.png + pentobi-32.png + pentobi-backward.png + pentobi-backward-16.png + pentobi-beginning.png + pentobi-beginning-16.png + pentobi-computer-colors.png + pentobi-computer-colors-16.png + pentobi-end.png + pentobi-end-16.png + pentobi-flip-horizontal.png + pentobi-flip-vertical.png + pentobi-forward.png + pentobi-forward-16.png + pentobi-newgame.png + pentobi-newgame-16.png + pentobi-next-piece.png + pentobi-next-variation.png + pentobi-next-variation-16.png + pentobi-piece-clear.png + pentobi-play.png + pentobi-play-16.png + pentobi-previous-piece.png + pentobi-previous-variation.png + pentobi-previous-variation-16.png + pentobi-rated-game.png + pentobi-rated-game-16.png + pentobi-rotate-left.png + pentobi-rotate-right.png + pentobi-undo.png + pentobi-undo-16.png + ) + +set(pentobi_TS + translations/pentobi.ts + translations/pentobi_de.ts + ) + +# Create PNG icons from SVG icons using the helper program src/convert +file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/icons) +file(COPY resources.qrc DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) +foreach(icon ${pentobi_ICNS}) + string(REPLACE ".png" ".svg" svgicon ${icon}) + add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/icons/${icon}" + COMMAND convert ${CMAKE_CURRENT_SOURCE_DIR}/icons/${svgicon} + ${CMAKE_CURRENT_BINARY_DIR}/icons/${icon} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/icons/${svgicon} + ) +endforeach() +qt5_add_resources(pentobi_RC_SRCS ${CMAKE_CURRENT_BINARY_DIR}/resources.qrc + OPTIONS -no-compress) +file(COPY resources_2x.qrc DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) +foreach(icon ${pentobi_ICNS}) +string(REPLACE ".png" ".svg" svgicon ${icon}) +string(REPLACE ".png" "@2x.png" hdpiicon ${icon}) +add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/icons/${hdpiicon}" + COMMAND convert --hdpi ${CMAKE_CURRENT_SOURCE_DIR}/icons/${svgicon} + ${CMAKE_CURRENT_BINARY_DIR}/icons/${hdpiicon} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/icons/${svgicon} +) +endforeach() +qt5_add_resources(pentobi_RC_SRCS + ${CMAKE_CURRENT_BINARY_DIR}/resources_2x.qrc OPTIONS -no-compress) + +qt5_add_translation(pentobi_QM_SRCS ${pentobi_TS}) + +add_executable(pentobi WIN32 + ${pentobi_SRCS} + ${pentobi_QM_SRCS} + ${pentobi_RC_SRCS} + ) + + +if(MINGW AND (CMAKE_SIZEOF_VOID_P EQUAL "4")) + set_target_properties(pentobi PROPERTIES LINK_FLAGS -Wl,--large-address-aware) +endif() + +target_link_libraries(pentobi + pentobi_gui + pentobi_mcts + pentobi_base + boardgame_base + boardgame_sgf + boardgame_util + boardgame_sys + ) + +target_link_libraries(pentobi Qt5::Widgets Qt5::Concurrent) + +if(CMAKE_THREAD_LIBS_INIT) + target_link_libraries(pentobi ${CMAKE_THREAD_LIBS_INIT}) +endif() + +install(TARGETS pentobi DESTINATION ${CMAKE_INSTALL_BINDIR}) + +# Install translation files. If you change the destination, you need to +# update the default for PENTOBI_TRANSLATIONS in the main CMakeLists.txt +install(FILES ${pentobi_QM_SRCS} + DESTINATION ${CMAKE_INSTALL_DATADIR}/pentobi/translations) + +install(DIRECTORY help DESTINATION ${CMAKE_INSTALL_DATAROOTDIR} + FILES_MATCHING PATTERN "*.css" PATTERN "*.html" PATTERN "*.png" PATTERN "*.jpg") + +if(MSVC) + configure_file(pentobi.conf.in Debug/pentobi.conf @ONLY) + configure_file(pentobi.conf.in Release/pentobi.conf @ONLY) +else() + configure_file(pentobi.conf.in pentobi.conf @ONLY) +endif() diff --git a/src/pentobi/Main.cpp b/src/pentobi/Main.cpp new file mode 100644 index 0000000..a6b0f92 --- /dev/null +++ b/src/pentobi/Main.cpp @@ -0,0 +1,210 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/Main.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include "Application.h" +#include "MainWindow.h" + +#ifdef Q_OS_WIN +#include +#include +#include +#include +#endif + +using libboardgame_util::LogInitializer; +using libboardgame_util::RandomGenerator; + +//----------------------------------------------------------------------------- + +namespace { + +void redirectStdErr() +{ +#ifdef Q_OS_WIN + CONSOLE_SCREEN_BUFFER_INFO info; + AllocConsole(); + GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info); + info.dwSize.Y = 500; + SetConsoleScreenBufferSize(GetStdHandle(STD_OUTPUT_HANDLE), info.dwSize); + auto stdErrHandle = (intptr_t)GetStdHandle(STD_ERROR_HANDLE); + int conHandle = _open_osfhandle(stdErrHandle, _O_TEXT); + auto f = _fdopen(conHandle, "w"); + *stderr = *f; + setvbuf(stderr, NULL, _IONBF, 0); + ios::sync_with_stdio(); +#endif +} + +} // namespace + +//----------------------------------------------------------------------------- + +int main(int argc, char* argv[]) +{ + LogInitializer log_initializer; + Q_INIT_RESOURCE(libpentobi_gui_resources); +#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) + QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); +#endif + QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); + Application app(argc, argv); + app.setOrganizationName("Pentobi"); + app.setApplicationName("Pentobi"); + Q_INIT_RESOURCE(libpentobi_gui_resources_2x); + try + { + // For some reason, labels in the status bar have a border on + // Windows 7 with Qt 4.8. We don't want that. + app.setStyleSheet("QStatusBar::item { border: 0px solid black }"); + + // Allow the user to override installation paths with a config file in + // the directory of the executable to test it without installation + QString helpDir; + QString booksDir; + QString translationsPentobiDir; + QString translationsLibPentobiGuiDir; + QString appDir = app.applicationDirPath(); +#ifdef PENTOBI_HELP_DIR + helpDir = PENTOBI_HELP_DIR; +#endif + if (helpDir.isEmpty()) + helpDir = appDir + "/help"; +#ifdef PENTOBI_BOOKS_DIR + booksDir = PENTOBI_BOOKS_DIR; +#endif + if (booksDir.isEmpty()) + booksDir = appDir + "/books"; +#ifdef PENTOBI_TRANSLATIONS + translationsPentobiDir = PENTOBI_TRANSLATIONS; + translationsLibPentobiGuiDir = PENTOBI_TRANSLATIONS; +#endif + if (translationsPentobiDir.isEmpty()) + translationsPentobiDir = appDir + "/translations"; + if (translationsLibPentobiGuiDir.isEmpty()) + translationsLibPentobiGuiDir = appDir + "/translations"; + QString overrideConfigFile = appDir + "/pentobi.conf"; + if (QFileInfo::exists(overrideConfigFile)) + { + QSettings settings(overrideConfigFile, QSettings::IniFormat); + helpDir = settings.value("HelpDir", helpDir).toString(); + booksDir = settings.value("BooksDir", booksDir).toString(); + translationsPentobiDir = + settings.value("TranslationsPentobiDir", + translationsPentobiDir).toString(); + translationsLibPentobiGuiDir = + settings.value("TranslationsLibPentobiGuiDir", + translationsLibPentobiGuiDir).toString(); + } + + QTranslator qtTranslator; + QString qtTranslationPath = + QLibraryInfo::location(QLibraryInfo::TranslationsPath); + QString locale = QLocale::system().name(); + qtTranslator.load("qt_" + locale, qtTranslationPath); + app.installTranslator(&qtTranslator); + QTranslator libPentobiGuiTranslator; + libPentobiGuiTranslator.load("libpentobi_gui_" + locale, + translationsLibPentobiGuiDir); + app.installTranslator(&libPentobiGuiTranslator); + QTranslator pentobiTranslator; + pentobiTranslator.load("pentobi_" + locale, translationsPentobiDir); + app.installTranslator(&pentobiTranslator); + + QCommandLineParser parser; + auto maxSupportedLevel = Player::max_supported_level; + QCommandLineOption optionMaxLevel("maxlevel", + "Set maximum level to .", "n", + QString::number(maxSupportedLevel)); + parser.addOption(optionMaxLevel); + QCommandLineOption optionNoBook("nobook"); + parser.addOption(optionNoBook); + QCommandLineOption optionNoDelay("nodelay"); + parser.addOption(optionNoDelay); + QCommandLineOption optionSeed("seed", "Set random seed to .", "n"); + parser.addOption(optionSeed); + QCommandLineOption optionThreads("threads", "Use threads.", "n"); + parser.addOption(optionThreads); + QCommandLineOption optionVerbose("verbose"); + parser.addOption(optionVerbose); + parser.process(app); + bool ok; + auto maxLevel = parser.value(optionMaxLevel).toUInt(&ok); + if (! ok || maxLevel < 1 || maxLevel > maxSupportedLevel) + throw runtime_error("--maxlevel must be between 1 and " + + to_string(maxSupportedLevel)); + unsigned threads = 0; + if (parser.isSet(optionThreads)) + { + threads = parser.value(optionThreads).toUInt(&ok); + if (! ok || threads == 0) + throw runtime_error("--threads must be greater zero."); + if (! libpentobi_mcts::SearchParamConst::multithread + && threads > 1) + throw runtime_error("This version of Pentobi was compiled" + " without support for multi-threading."); + } + if (! parser.isSet(optionVerbose)) + libboardgame_util::disable_logging(); + else + redirectStdErr(); + if (parser.isSet(optionSeed)) + { + RandomGenerator::ResultType seed = + parser.value(optionSeed).toUInt(&ok); + if (! ok) + throw runtime_error("--seed must be a positive number"); + RandomGenerator::set_global_seed(seed); + } + bool noBook = parser.isSet(optionNoBook); + QString initialFile; + auto args = parser.positionalArguments(); + if (! args.empty()) + initialFile = args.at(0); + QSettings settings; + auto variantString = settings.value("variant", "").toString(); + Variant variant; + if (! parse_variant_id(variantString.toLocal8Bit().constData(), + variant)) + variant = Variant::duo; + try + { + MainWindow mainWindow(variant, initialFile, helpDir, maxLevel, + booksDir, noBook, threads); + if (parser.isSet(optionNoDelay)) + mainWindow.setNoDelay(); + mainWindow.show(); + return app.exec(); + } + catch (bad_alloc&) + { + // bad_alloc is an expected error because libpentobi_mcts::Player + // requires a larger amount of memory and an error message should + // be shown to the user. It needs to be handled here because it + // needs the translators being installed for the error message. + QMessageBox::critical( + nullptr, app.translate("main", "Pentobi"), + app.translate("main", "Not enough memory.")); + } + } + catch (const exception& e) + { + cerr << "Error: " << e.what() << '\n'; + return 1; + } +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/MainWindow.cpp b/src/pentobi/MainWindow.cpp new file mode 100644 index 0000000..b74b6eb --- /dev/null +++ b/src/pentobi/MainWindow.cpp @@ -0,0 +1,3466 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/MainWindow.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "MainWindow.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "AnalyzeGameWindow.h" +#include "AnalyzeSpeedDialog.h" +#include "RatingDialog.h" +#include "ShowMessage.h" +#include "Util.h" +#include "libboardgame_sgf/SgfUtil.h" +#include "libboardgame_sgf/TreeReader.h" +#include "libboardgame_util/Assert.h" +#include "libpentobi_base/TreeUtil.h" +#include "libpentobi_base/PentobiTreeWriter.h" +#include "libpentobi_gui/ComputerColorDialog.h" +#include "libpentobi_gui/GameInfoDialog.h" +#include "libpentobi_gui/GuiBoard.h" +#include "libpentobi_gui/GuiBoardUtil.h" +#include "libpentobi_gui/HelpWindow.h" +#include "libpentobi_gui/InitialRatingDialog.h" +#include "libpentobi_gui/LeaveFullscreenButton.h" +#include "libpentobi_gui/OrientationDisplay.h" +#include "libpentobi_gui/PieceSelector.h" +#include "libpentobi_gui/SameHeightLayout.h" +#include "libpentobi_gui/ScoreDisplay.h" +#include "libpentobi_gui/Util.h" + +using Util::getPlayerString; +using libboardgame_sgf::InvalidTree; +using libboardgame_sgf::TreeReader; +using libboardgame_sgf::util::back_to_main_variation; +using libboardgame_sgf::util::beginning_of_branch; +using libboardgame_sgf::util::find_next_comment; +using libboardgame_sgf::util::get_last_node; +using libboardgame_sgf::util::get_move_annotation; +using libboardgame_sgf::util::get_variation_string; +using libboardgame_sgf::util::has_comment; +using libboardgame_sgf::util::has_earlier_variation; +using libboardgame_sgf::util::is_main_variation; +using libboardgame_util::clear_abort; +using libboardgame_util::get_abort; +using libboardgame_util::set_abort; +using libboardgame_util::trim_right; +using libboardgame_util::ArrayList; +using libpentobi_base::BoardType; +using libpentobi_base::MoveInfo; +using libpentobi_base::MoveInfoExt; +using libpentobi_base::PieceInfo; +using libpentobi_base::PieceSet; +using libpentobi_base::PentobiTree; +using libpentobi_base::PentobiTreeWriter; +using libpentobi_base::ScoreType; +using libpentobi_base::tree_util::get_move_number; +using libpentobi_base::tree_util::get_moves_left; +using libpentobi_base::tree_util::get_position_info; +using libpentobi_mcts::Search; + +//----------------------------------------------------------------------------- + +namespace { + +/** Create a button for manipulating piece orientation. */ +QToolButton* createOBoxToolButton(QAction* action) +{ + auto button = new QToolButton; + button->setDefaultAction(action); + button->setAutoRaise(true); + // No focus, there are faster keyboard shortcuts for manipulating pieces + button->setFocusPolicy(Qt::NoFocus); + // For some reason, toolbuttons are very small in Ubuntu Unity if outside + // a toolbar (tested with Ubuntu 15.10) + button->setMinimumSize(28, 28); + return button; +} + +/** Return auto-save file name as a native path name. */ +QString getAutoSaveFile() +{ + return Util::getDataDir() + QDir::separator() + "autosave.blksgf"; +} + +bool hasCurrentVariationOtherMoves(const PentobiTree& tree, + const SgfNode& current) +{ + auto node = current.get_parent_or_null(); + while (node) + { + if (! tree.get_move(*node).is_null()) + return true; + node = node->get_parent_or_null(); + } + node = current.get_first_child_or_null(); + while (node) + { + if (! tree.get_move(*node).is_null()) + return true; + node = node->get_first_child_or_null(); + } + return false; +} + +void initToolBarText(QToolBar* toolBar) +{ + QSettings settings; + auto toolBarText = settings.value("toolbar_text", "system").toString(); + if (toolBarText == "no_text") + toolBar->setToolButtonStyle(Qt::ToolButtonIconOnly); + else if (toolBarText == "beside_icons") + toolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + else if (toolBarText == "below_icons") + toolBar->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); + else if (toolBarText == "text_only") + toolBar->setToolButtonStyle(Qt::ToolButtonTextOnly); + else + toolBar->setToolButtonStyle(Qt::ToolButtonFollowStyle); +} + +void setIcon(QAction* action, const QString& name) +{ + QIcon icon(QString(":/pentobi/icons/%1.png").arg(name)); + QString file16 = QString(":/pentobi/icons/%1-16.png").arg(name); + if (QFile::exists(file16)) + icon.addFile(file16, QSize(16, 16)); + action->setIcon(icon); +} + +/** Simple heuristic that prefers moves with more score points. + Used for sorting the list used in Find Move. */ +ScoreType getHeuristic(const Board& bd, Move mv) +{ + return bd.get_piece_info(bd.get_move_piece(mv)).get_score_points(); +} + +} // namespace + +//----------------------------------------------------------------------------- + +MainWindow::MainWindow(Variant variant, const QString& initialFile, + const QString& helpDir, unsigned maxLevel, + const QString& booksDir, bool noBook, + unsigned nuThreads) + : m_game(variant), + m_bd(m_game.get_board()), + m_helpDir(helpDir) +{ + if (maxLevel > Player::max_supported_level) + maxLevel = Player::max_supported_level; + if (maxLevel < 1) + maxLevel = 1; + m_maxLevel = maxLevel; + Util::initDataDir(); + QSettings settings; + m_history.reset(new RatingHistory(variant)); + createActions(); + restoreLevel(variant); + setCentralWidget(createCentralWidget()); + initPieceSelectors(); + m_moveNumber = new QLabel; + statusBar()->addPermanentWidget(m_moveNumber); + m_setupModeLabel = new QLabel(tr("Setup mode")); + statusBar()->addWidget(m_setupModeLabel); + m_setupModeLabel->hide(); + m_ratedGameLabelText = new QLabel(tr("Rated game")); + statusBar()->addWidget(m_ratedGameLabelText); + m_ratedGameLabelText->hide(); + initGame(); + m_player.reset(new Player(variant, maxLevel, + booksDir.toLocal8Bit().constData(), nuThreads)); + m_player->get_search().set_callback(bind(&MainWindow::searchCallback, + this, placeholders::_1, + placeholders::_2)); + m_player->set_use_book(! noBook); + createToolBar(); + connect(&m_genMoveWatcher, SIGNAL(finished()), + SLOT(genMoveFinished())); + connect(m_guiBoard, SIGNAL(play(Color, Move)), + SLOT(placePiece(Color, Move))); + connect(m_guiBoard, SIGNAL(pointClicked(Point)), + SLOT(pointClicked(Point))); + connect(m_actionMovePieceLeft, SIGNAL(triggered()), + m_guiBoard, SLOT(movePieceLeft())); + connect(m_actionMovePieceRight, SIGNAL(triggered()), + m_guiBoard, SLOT(movePieceRight())); + connect(m_actionMovePieceUp, SIGNAL(triggered()), + m_guiBoard, SLOT(movePieceUp())); + connect(m_actionMovePieceDown, SIGNAL(triggered()), + m_guiBoard, SLOT(movePieceDown())); + connect(m_actionPlacePiece, SIGNAL(triggered()), + m_guiBoard, SLOT(placePiece())); + createMenu(); + qApp->installEventFilter(this); + updateRecentFiles(); + auto marking = settings.value("move_marking", "last_dot").toString(); + if (marking == "all_number") + m_actionMoveMarkingAllNumber->setChecked(true); + else if (marking == "last_dot") + m_actionMoveMarkingLastDot->setChecked(true); + else if (marking == "last_number") + m_actionMoveMarkingLastNumber->setChecked(true); + else + m_actionMoveMarkingNone->setChecked(true); + auto coordinates = settings.value("coordinates", false).toBool(); + m_guiBoard->setCoordinates(coordinates); + m_actionCoordinates->setChecked(coordinates); + auto showToolbar = settings.value("toolbar", true).toBool(); + findChild()->setVisible(showToolbar); + m_menuToolBarText->setEnabled(showToolbar); + m_actionShowToolbar->setChecked(showToolbar); + auto showVariations = settings.value("show_variations", true).toBool(); + m_actionShowVariations->setChecked(showVariations); + initVariantActions(); + QIcon icon; + icon.addFile(":/pentobi/icons/pentobi.png"); + icon.addFile(":/pentobi/icons/pentobi-16.png"); + icon.addFile(":/pentobi/icons/pentobi-32.png"); + setWindowIcon(icon); + + bool centerOnScreen = false; + QRect screenGeometry = QApplication::desktop()->screenGeometry(); + if (restoreGeometry(settings.value("geometry").toByteArray())) + { + if (! screenGeometry.contains(geometry())) + { + if (width() > screenGeometry.width() + || height() > screenGeometry.height()) + adjustSize(); + centerOnScreen = true; + } + } + else + { + adjustSize(); + centerOnScreen = true; + } + if (centerOnScreen) + { + int x = (screenGeometry.width() - width()) / 2; + int y = (screenGeometry.height() - height()) / 2; + move(x, y); + } + + auto showComment = settings.value("show_comment", false).toBool(); + m_comment->setVisible(showComment); + if (showComment) + m_splitter->restoreState( + settings.value("splitter_state").toByteArray()); + m_actionShowComment->setChecked(showComment); + updateWindow(true); + clearFile(); + if (! initialFile.isEmpty()) + { + if (open(initialFile)) + rememberFile(initialFile); + } + else + { + QString autoSaveFile = getAutoSaveFile(); + if (QFile(autoSaveFile).exists()) + { + open(autoSaveFile, true); + m_isAutoSaveLoaded = true; + deleteAutoSaveFile(); + m_gameFinished = m_bd.is_game_over(); + updateWindow(true); + if (settings.value("autosave_rated", false).toBool()) + QMetaObject::invokeMethod(this, "continueRatedGame", + Qt::QueuedConnection); + } + } +} + +MainWindow::~MainWindow() +{ +} + +void MainWindow::about() +{ + QMessageBox::about(this, tr("About Pentobi"), + "" + "

" + tr("Pentobi") + "

" + "

" + tr("Version %1").arg(getVersion()) + "

" + "

" + + tr("Computer opponent for the board game Blokus.") + + "
" + + tr("© 2011–%1 Markus Enzenberger").arg(2017) + + + "
" + + "http://pentobi.sourceforge.net" + "

"); +} + +void MainWindow::analyzeGame() +{ + if (! is_main_variation(m_game.get_current())) + { + showInfo(tr("Game analysis is only possible in the main variation.")); + return; + } + AnalyzeSpeedDialog dialog(this, tr("Analyze Game")); + if (! dialog.exec()) + return; + int speed = dialog.getSpeedValue(); + cancelThread(); + if (m_analyzeGameWindow) + delete m_analyzeGameWindow; + m_analyzeGameWindow = new AnalyzeGameWindow(this); + // Make sure all action shortcuts work when the analyze dialog has the + // focus apart from m_actionLeaveFullscreen because the Esc key is used + // to close the dialog + m_analyzeGameWindow->addActions(actions()); + m_analyzeGameWindow->removeAction(m_actionLeaveFullscreen); + m_analyzeGameWindow->show(); + m_isAnalyzeRunning = true; + connect(m_analyzeGameWindow->analyzeGameWidget, SIGNAL(finished()), + SLOT(analyzeGameFinished())); + connect(m_analyzeGameWindow->analyzeGameWidget, + SIGNAL(gotoPosition(Variant,const vector&)), + SLOT(gotoPosition(Variant,const vector&))); + size_t nuSimulations; + switch (speed) + { + case 0: + nuSimulations = 6000; + break; + case 1: + nuSimulations = 24000; + break; + default: + nuSimulations = 96000; + } + m_analyzeGameWindow->analyzeGameWidget->start( + m_game, m_player->get_search(), nuSimulations); +} + +void MainWindow::analyzeGameFinished() +{ + m_analyzeGameWindow->analyzeGameWidget->setCurrentPosition( + m_game, m_game.get_current()); + m_isAnalyzeRunning = false; +} + +/** Call to Player::genmove() that runs in a different thread. */ +MainWindow::GenMoveResult MainWindow::asyncGenMove(Color c, int genMoveId, + bool playSingleMove) +{ + QElapsedTimer timer; + timer.start(); + GenMoveResult result; + result.playSingleMove = playSingleMove; + result.color = c; + result.genMoveId = genMoveId; + result.move = m_player->genmove(m_bd, c); + auto elapsed = timer.elapsed(); + // Enforce minimum thinking time of 0.8 sec + if (elapsed < 800 && ! m_noDelay) + QThread::msleep(800 - elapsed); + return result; +} + +void MainWindow::badMove(bool checked) +{ + if (! checked) + return; + m_game.set_bad_move(); + updateWindow(false); +} + +void MainWindow::backward() +{ + gotoNode(m_game.get_current().get_parent_or_null()); +} + +void MainWindow::backToMainVariation() +{ + gotoNode(back_to_main_variation(m_game.get_current())); +} + +void MainWindow::beginning() +{ + gotoNode(m_game.get_root()); +} + +void MainWindow::beginningOfBranch() +{ + gotoNode(beginning_of_branch(m_game.get_current())); +} + +void MainWindow::cancelThread() +{ + if (m_isAnalyzeRunning) + { + // This should never happen because AnalyzeGameWindow protects the + // parent with a modal progress dialog while it is running. However, + // due to bugs in Unity 2D (tested with Ubuntu 11.04 and 11.10), the + // global menu can still trigger menu item events. + m_analyzeGameWindow->analyzeGameWidget->cancel(); + } + if (! m_isGenMoveRunning) + return; + // After waitForFinished() returns, we can be sure that the move generation + // is no longer running, but we will still receive the finished event. + // Increasing m_genMoveId will make genMoveFinished() ignore the event. + ++m_genMoveId; + set_abort(); + m_genMoveWatcher.waitForFinished(); + m_isGenMoveRunning = false; + clearStatus(); + setCursor(QCursor(Qt::ArrowCursor)); + m_actionInterrupt->setEnabled(false); + m_actionNextPiece->setEnabled(true); + m_actionPlay->setEnabled(true); + m_actionPlaySingleMove->setEnabled(true); + m_actionPreviousPiece->setEnabled(true); +} + +void MainWindow::checkComputerMove() +{ + if (m_autoPlay && isComputerToPlay() && ! m_bd.is_game_over() + && ! m_isGenMoveRunning) + genMove(); +} + +bool MainWindow::checkSave() +{ + if (! m_file.isEmpty()) + { + if (! m_game.is_modified()) + return true; + QMessageBox msgBox(this); + initQuestion(msgBox, tr("The file has been modified."), + tr("Do you want to save your changes?")); + // Don't use QMessageBox::Discard because on some platforms it uses the + // text "Close without saving" which implies that the window would be + // closed + auto discardButton = + msgBox.addButton(tr("&Don't Save"), QMessageBox::DestructiveRole); + auto saveButton = msgBox.addButton(QMessageBox::Save); + auto cancelButton = msgBox.addButton(QMessageBox::Cancel); + msgBox.setDefaultButton(cancelButton); + msgBox.exec(); + auto result = msgBox.clickedButton(); + if (result == saveButton) + { + save(); + return true; + } + return result == discardButton; + } + // Don't ask if game should be saved if it was finished because the user + // might only want to play and never save games. + if (m_game.get_root().has_children() && ! m_gameFinished) + { + QMessageBox msgBox(this); + initQuestion(msgBox, tr("The current game is not finished."), + tr("Do you want to abort the game?")); + auto abortGameButton = + msgBox.addButton(tr("&Abort Game"), QMessageBox::DestructiveRole); + auto cancelButton = msgBox.addButton(QMessageBox::Cancel); + msgBox.setDefaultButton(cancelButton); + msgBox.exec(); + if (msgBox.clickedButton() != abortGameButton) + return false; + return true; + } + return true; +} + +bool MainWindow::checkQuit() +{ + if (! m_file.isEmpty() && m_game.is_modified()) + { + QMessageBox msgBox(this); + initQuestion(msgBox, tr("The file has been modified."), + tr("Do you want to save your changes?")); + auto discardButton = msgBox.addButton(QMessageBox::Discard); + auto saveButton = msgBox.addButton(QMessageBox::Save); + auto cancelButton = msgBox.addButton(QMessageBox::Cancel); + msgBox.setDefaultButton(cancelButton); + msgBox.exec(); + auto result = msgBox.clickedButton(); + if (result == saveButton) + { + save(); + return true; + } + return result == discardButton; + } + cancelThread(); + QSettings settings; + if (m_file.isEmpty() && ! m_gameFinished + && (m_game.is_modified() || m_isAutoSaveLoaded)) + { + writeGame(getAutoSaveFile().toLocal8Bit().constData()); + settings.setValue("autosave_rated", m_isRated); + if (m_isRated) + settings.setValue("autosave_rated_color", + m_ratedGameColor.to_int()); + } + if (! isFullScreen()) + settings.setValue("geometry", saveGeometry()); + if (m_comment->isVisible()) + settings.setValue("splitter_state", m_splitter->saveState()); + return true; +} + +void MainWindow::clearFile() +{ + setFile(""); +} + +void MainWindow::clearPiece() +{ + m_actionRotateClockwise->setEnabled(false); + m_actionRotateAnticlockwise->setEnabled(false); + m_actionFlipHorizontally->setEnabled(false); + m_actionFlipVertically->setEnabled(false); + m_actionClearPiece->setEnabled(false); + m_guiBoard->clearPiece(); + m_orientationDisplay->clearPiece(); +} + +void MainWindow::clearStatus() +{ + statusBar()->clearMessage(); +} + +void MainWindow::closeEvent(QCloseEvent* event) +{ + if (checkQuit()) + event->accept(); + else + event->ignore(); +} + +void MainWindow::commentChanged() +{ + if (m_ignoreCommentTextChanged) + return; + QString comment = m_comment->toPlainText(); + if (comment.isEmpty()) + m_game.set_comment(""); + else + { + string charset = m_game.get_root().get_property("CA", ""); + string value = Util::convertSgfValueFromQString(comment, charset); + value = trim_right(value); + m_game.set_comment(value); + } + updateWindow(false); +} + +void MainWindow::computerColors() +{ + ColorMap oldComputerColors = m_computerColors; + ComputerColorDialog dialog(this, m_bd.get_variant(), m_computerColors); + dialog.exec(); + auto nu_colors = m_bd.get_nu_nonalt_colors(); + + bool computerNone = true; + for (Color c : Color::Range(nu_colors)) + if (m_computerColors[c]) + { + computerNone = false; + break; + } + QSettings settings; + settings.setValue("computer_color_none", computerNone); + + // Enable autoplay only if any color has changed because that means that + // the user probably wants to continue playing, otherwise the user could + // have only opened the dialog to check the current settings + for (Color c : Color::Range(nu_colors)) + if (m_computerColors[c] != oldComputerColors[c]) + { + m_autoPlay = true; + break; + } + + checkComputerMove(); +} + +bool MainWindow::computerPlaysAll() const +{ + for (Color c : Color::Range(m_bd.get_nu_nonalt_colors())) + if (! m_computerColors[c]) + return false; + return true; +} + +void MainWindow::continueRatedGame() +{ + auto nuColors = m_bd.get_nu_colors(); + QSettings settings; + auto color = + static_cast( + settings.value("autosave_rated_color", 0).toUInt()); + if (color >= nuColors) + return; + m_ratedGameColor = Color(color); + m_computerColors.fill(true); + for (Color c : Color::Range(nuColors)) + if (m_bd.is_same_player(c, m_ratedGameColor)) + m_computerColors[c] = false; + setRated(true); + updateWindow(false); + showInfo(tr("Continuing unfinished rated game."), + tr("You play %1 in this game.") + .arg(getPlayerString(m_bd.get_variant(), m_ratedGameColor))); + m_autoPlay = true; + checkComputerMove(); +} + +void MainWindow::coordinates(bool checked) +{ + m_guiBoard->setCoordinates(checked); + QSettings settings; + settings.setValue("coordinates", checked); +} + +QAction* MainWindow::createAction(const QString& text) +{ + auto action = new QAction(text, this); + // Add all actions also to main window. if an action is only added to + // the menu bar, shortcuts won't work in fullscreen mode because the menu + // is not visible in fullscreen mode + addAction(action); + return action; +} + +QAction* MainWindow::createActionLevel(unsigned level, const QString& text) +{ + auto action = createAction(text); + action->setCheckable(true); + action->setActionGroup(m_actionGroupLevel); + action->setData(level); + connect(action, SIGNAL(triggered(bool)), SLOT(levelTriggered(bool))); + return action; +} + +void MainWindow::createActions() +{ + m_actionGroupVariant = new QActionGroup(this); + m_actionGroupLevel = new QActionGroup(this); + auto groupMoveMarking = new QActionGroup(this); + auto groupMoveAnnotation = new QActionGroup(this); + auto groupToolBarText = new QActionGroup(this); + + m_actionAbout = createAction(tr("&About Pentobi")); + connect(m_actionAbout, SIGNAL(triggered()), SLOT(about())); + + m_actionAnalyzeGame = createAction(tr("&Analyze Game...")); + connect(m_actionAnalyzeGame, SIGNAL(triggered()), SLOT(analyzeGame())); + + m_actionBackward = createAction(tr("B&ackward")); + m_actionBackward->setToolTip(tr("Go one move backward")); + m_actionBackward->setPriority(QAction::LowPriority); + setIcon(m_actionBackward, "pentobi-backward"); + m_actionBackward->setShortcut(QKeySequence::MoveToPreviousWord); + connect(m_actionBackward, SIGNAL(triggered()), SLOT(backward())); + + m_actionBackToMainVariation = createAction(tr("Back to &Main Variation")); + m_actionBackToMainVariation->setShortcut(QString("Ctrl+M")); + connect(m_actionBackToMainVariation, SIGNAL(triggered()), + SLOT(backToMainVariation())); + + m_actionBadMove = createAction(tr("&Bad")); + m_actionBadMove->setActionGroup(groupMoveAnnotation); + m_actionBadMove->setCheckable(true); + connect(m_actionBadMove, SIGNAL(triggered(bool)), SLOT(badMove(bool))); + + m_actionBeginning = createAction(tr("&Beginning")); + m_actionBeginning->setToolTip(tr("Go to beginning of game")); + m_actionBeginning->setPriority(QAction::LowPriority); + setIcon(m_actionBeginning, "pentobi-beginning"); + m_actionBeginning->setShortcut(QKeySequence::MoveToStartOfDocument); + connect(m_actionBeginning, SIGNAL(triggered()), SLOT(beginning())); + + m_actionBeginningOfBranch = createAction(tr("Beginning of Bran&ch")); + m_actionBeginningOfBranch->setShortcut(QString("Ctrl+B")); + connect(m_actionBeginningOfBranch, SIGNAL(triggered()), + SLOT(beginningOfBranch())); + + m_actionClearPiece = createAction(tr("Clear Piece")); + setIcon(m_actionClearPiece, "pentobi-piece-clear"); + m_actionClearPiece->setShortcut(QString("0")); + connect(m_actionClearPiece, SIGNAL(triggered()), SLOT(clearPiece())); + + m_actionComputerColors = createAction(tr("&Computer Colors")); + m_actionComputerColors->setShortcut(QString("Ctrl+U")); + m_actionComputerColors->setToolTip( + tr("Set the colors played by the computer")); + setIcon(m_actionComputerColors, "pentobi-computer-colors"); + connect(m_actionComputerColors, SIGNAL(triggered()), + SLOT(computerColors())); + + m_actionCoordinates = createAction(tr("C&oordinates")); + m_actionCoordinates->setCheckable(true); + connect(m_actionCoordinates, SIGNAL(triggered(bool)), + SLOT(coordinates(bool))); + + m_actionDeleteAllVariations = createAction(tr("&Delete All Variations")); + connect(m_actionDeleteAllVariations, SIGNAL(triggered()), + SLOT(deleteAllVariations())); + + m_actionDoubtfulMove = createAction(tr("&Doubtful")); + m_actionDoubtfulMove->setActionGroup(groupMoveAnnotation); + m_actionDoubtfulMove->setCheckable(true); + connect(m_actionDoubtfulMove, SIGNAL(triggered(bool)), + SLOT(doubtfulMove(bool))); + + m_actionEnd = createAction(tr("&End")); + m_actionEnd->setToolTip(tr("Go to end of moves")); + m_actionEnd->setPriority(QAction::LowPriority); + m_actionEnd->setShortcut(QKeySequence::MoveToEndOfDocument); + setIcon(m_actionEnd, "pentobi-end"); + connect(m_actionEnd, SIGNAL(triggered()), SLOT(end())); + + m_actionExportAsciiArt = createAction(tr("&ASCII Art")); + connect(m_actionExportAsciiArt, SIGNAL(triggered()), + SLOT(exportAsciiArt())); + + m_actionExportImage = createAction(tr("I&mage")); + connect(m_actionExportImage, SIGNAL(triggered()), SLOT(exportImage())); + + m_actionFindMove = createAction(tr("&Find Move")); + m_actionFindMove->setShortcut(QString("F6")); + connect(m_actionFindMove, SIGNAL(triggered()), SLOT(findMove())); + + m_actionFindNextComment = createAction(tr("Find Next &Comment")); + m_actionFindNextComment->setShortcut(QString("F3")); + connect(m_actionFindNextComment, SIGNAL(triggered()), + SLOT(findNextComment())); + + m_actionFlipHorizontally = createAction(tr("Flip Horizontally")); + setIcon(m_actionFlipHorizontally, "pentobi-flip-horizontal"); + connect(m_actionFlipHorizontally, SIGNAL(triggered()), + SLOT(flipHorizontally())); + + m_actionFlipVertically = createAction(tr("Flip Vertically")); + setIcon(m_actionFlipVertically, "pentobi-flip-vertical"); + + m_actionForward = createAction(tr("&Forward")); + m_actionForward->setToolTip(tr("Go one move forward")); + m_actionForward->setPriority(QAction::LowPriority); + m_actionForward->setShortcut(QKeySequence::MoveToNextWord); + setIcon(m_actionForward, "pentobi-forward"); + connect(m_actionForward, SIGNAL(triggered()), SLOT(forward())); + + m_actionFullscreen = createAction(tr("&Fullscreen")); + // Don't use QKeySequence::Fullscreen, it is Ctrl-F11 on Linux but that + // doesn't work in Xfce + m_actionFullscreen->setShortcut(QString("F11")); + connect(m_actionFullscreen, SIGNAL(triggered()), SLOT(fullscreen())); + + m_actionGameInfo = createAction(tr("Ga&me Info")); + m_actionGameInfo->setShortcut(QString("Ctrl+I")); + connect(m_actionGameInfo, SIGNAL(triggered()), SLOT(gameInfo())); + + m_actionGoodMove = createAction(tr("&Good")); + m_actionGoodMove->setActionGroup(groupMoveAnnotation); + m_actionGoodMove->setCheckable(true); + connect(m_actionGoodMove, SIGNAL(triggered(bool)), SLOT(goodMove(bool))); + + m_actionGotoMove = createAction(tr("&Go to Move...")); + m_actionGotoMove->setShortcut(QString("Ctrl+G")); + connect(m_actionGotoMove, SIGNAL(triggered()), SLOT(gotoMove())); + + m_actionHelp = createAction(tr("Pentobi &Help")); + m_actionHelp->setShortcut(QKeySequence::HelpContents); + connect(m_actionHelp, SIGNAL(triggered()), SLOT(help())); + + m_actionInterestingMove = createAction(tr("I&nteresting")); + m_actionInterestingMove->setActionGroup(groupMoveAnnotation); + m_actionInterestingMove->setCheckable(true); + connect(m_actionInterestingMove, SIGNAL(triggered(bool)), + SLOT(interestingMove(bool))); + + m_actionInterrupt = createAction(tr("St&op")); + m_actionInterrupt->setEnabled(false); + connect(m_actionInterrupt, SIGNAL(triggered()), SLOT(interrupt())); + + m_actionInterruptPlay = createAction(); + m_actionInterruptPlay->setShortcut(QString("Shift+Esc")); + connect(m_actionInterruptPlay, SIGNAL(triggered()), SLOT(interruptPlay())); + + m_actionKeepOnlyPosition = createAction(tr("&Keep Only Position")); + connect(m_actionKeepOnlyPosition, SIGNAL(triggered()), + SLOT(keepOnlyPosition())); + + m_actionKeepOnlySubtree = createAction(tr("Keep Only &Subtree")); + connect(m_actionKeepOnlySubtree, SIGNAL(triggered()), + SLOT(keepOnlySubtree())); + + m_actionLeaveFullscreen = createAction(tr("Leave Fullscreen")); + m_actionLeaveFullscreen->setShortcut(QString("Esc")); + connect(m_actionLeaveFullscreen, SIGNAL(triggered()), + SLOT(leaveFullscreen())); + + m_actionMakeMainVariation = createAction(tr("M&ake Main Variation")); + connect(m_actionMakeMainVariation, SIGNAL(triggered()), + SLOT(makeMainVariation())); + + m_actionMoveDownVariation = createAction(tr("Move Variation D&own")); + connect(m_actionMoveDownVariation, SIGNAL(triggered()), + SLOT(moveDownVariation())); + + m_actionMoveUpVariation = createAction(tr("Move Variation &Up")); + connect(m_actionMoveUpVariation, SIGNAL(triggered()), + SLOT(moveUpVariation())); + + static_assert(Player::max_supported_level <= 9, ""); + QString levelText[Player::max_supported_level] = + { tr("&1"), tr("&2"), tr("&3"), tr("&4"), tr("&5"), tr("&6"), + tr("&7"), tr("&8"), tr("&9") }; + for (unsigned i = 0; i < m_maxLevel; ++i) + createActionLevel(i + 1, levelText[i]); + connect(m_actionFlipVertically, SIGNAL(triggered()), + SLOT(flipVertically())); + + m_actionMoveMarkingAllNumber = createAction(tr("&All with Number")); + m_actionMoveMarkingAllNumber->setActionGroup(groupMoveMarking); + m_actionMoveMarkingAllNumber->setCheckable(true); + connect(m_actionMoveMarkingAllNumber, SIGNAL(triggered(bool)), + SLOT(setMoveMarkingAllNumber(bool))); + + m_actionMoveMarkingLastDot = createAction(tr("Last with &Dot")); + m_actionMoveMarkingLastDot->setActionGroup(groupMoveMarking); + m_actionMoveMarkingLastDot->setCheckable(true); + m_actionMoveMarkingLastDot->setChecked(true); + connect(m_actionMoveMarkingLastDot, SIGNAL(triggered(bool)), + SLOT(setMoveMarkingLastDot(bool))); + + m_actionMoveMarkingLastNumber = createAction(tr("&Last with Number")); + m_actionMoveMarkingLastNumber->setActionGroup(groupMoveMarking); + m_actionMoveMarkingLastNumber->setCheckable(true); + m_actionMoveMarkingLastNumber->setChecked(true); + connect(m_actionMoveMarkingLastNumber, SIGNAL(triggered(bool)), + SLOT(setMoveMarkingLastNumber(bool))); + + m_actionMoveMarkingNone = createAction(tr("&None", "move numbers")); + m_actionMoveMarkingNone->setActionGroup(groupMoveMarking); + m_actionMoveMarkingNone->setCheckable(true); + connect(m_actionMoveMarkingNone, SIGNAL(triggered(bool)), + SLOT(setMoveMarkingNone(bool))); + + m_actionMovePieceLeft = createAction(); + m_actionMovePieceLeft->setShortcut(QKeySequence::MoveToPreviousChar); + + m_actionMovePieceRight = createAction(); + m_actionMovePieceRight->setShortcut(QKeySequence::MoveToNextChar); + + m_actionMovePieceUp = createAction(); + m_actionMovePieceUp->setShortcut(QKeySequence::MoveToPreviousLine); + + m_actionMovePieceDown = createAction(); + m_actionMovePieceDown->setShortcut(QKeySequence::MoveToNextLine); + + m_actionNextPiece = createAction(tr("Next Piece")); + setIcon(m_actionNextPiece, "pentobi-next-piece"); + m_actionNextPiece->setShortcut(QString("+")); + connect(m_actionNextPiece, SIGNAL(triggered()), SLOT(nextPiece())); + + m_actionNextTransform = createAction(); + m_actionNextTransform->setShortcut(QString("Space")); + connect(m_actionNextTransform, SIGNAL(triggered()), SLOT(nextTransform())); + + m_actionNextVariation = createAction(tr("&Next Variation")); + m_actionNextVariation->setToolTip(tr("Go to next variation")); + m_actionNextVariation->setPriority(QAction::LowPriority); + m_actionNextVariation->setShortcut(QKeySequence::MoveToNextPage); + setIcon(m_actionNextVariation, "pentobi-next-variation"); + connect(m_actionNextVariation, SIGNAL(triggered()), SLOT(nextVariation())); + + m_actionNew = createAction(tr("&New")); + m_actionNew->setShortcut(QKeySequence::New); + m_actionNew->setToolTip(tr("Start a new game")); + setIcon(m_actionNew, "pentobi-newgame"); + connect(m_actionNew, SIGNAL(triggered()), SLOT(newGame())); + + m_actionNoMoveAnnotation = createAction(tr("N&one", "move annotation")); + m_actionNoMoveAnnotation->setActionGroup(groupMoveAnnotation); + m_actionNoMoveAnnotation->setCheckable(true); + connect(m_actionNoMoveAnnotation, SIGNAL(triggered(bool)), + SLOT(noMoveAnnotation(bool))); + + m_actionOpen = createAction(tr("&Open...")); + m_actionOpen->setShortcut(QKeySequence::Open); + connect(m_actionOpen, SIGNAL(triggered()), SLOT(open())); + m_actionPlacePiece = createAction(); + m_actionPlacePiece->setShortcut(QString("Return")); + + m_actionPlay = createAction(tr("&Play")); + m_actionPlay->setShortcut(QString("Ctrl+L")); + setIcon(m_actionPlay, "pentobi-play"); + connect(m_actionPlay, SIGNAL(triggered()), SLOT(play())); + + m_actionPlaySingleMove = createAction(tr("Play &Single Move")); + m_actionPlaySingleMove->setShortcut(QString("Ctrl+Shift+L")); + connect(m_actionPlaySingleMove, SIGNAL(triggered()), + SLOT(playSingleMove())); + + m_actionPreviousPiece = createAction(tr("Previous Piece")); + setIcon(m_actionPreviousPiece, "pentobi-previous-piece"); + m_actionPreviousPiece->setShortcut(QString("-")); + connect(m_actionPreviousPiece, SIGNAL(triggered()), + SLOT(previousPiece())); + + m_actionPreviousTransform = createAction(); + m_actionPreviousTransform->setShortcut(QString("Shift+Space")); + connect(m_actionPreviousTransform, SIGNAL(triggered()), + SLOT(previousTransform())); + + m_actionPreviousVariation = createAction(tr("&Previous Variation")); + m_actionPreviousVariation->setToolTip(tr("Go to previous variation")); + m_actionPreviousVariation->setPriority(QAction::LowPriority); + m_actionPreviousVariation->setShortcut(QKeySequence::MoveToPreviousPage); + setIcon(m_actionPreviousVariation, "pentobi-previous-variation"); + connect(m_actionPreviousVariation, SIGNAL(triggered()), + SLOT(previousVariation())); + + m_actionRatedGame = createAction(tr("&Rated Game")); + m_actionRatedGame->setToolTip(tr("Start a rated game")); + m_actionRatedGame->setShortcut(QString("Ctrl+Shift+N")); + setIcon(m_actionRatedGame, "pentobi-rated-game"); + connect(m_actionRatedGame, SIGNAL(triggered()), SLOT(ratedGame())); + + for (auto& action : m_actionRecentFile) + { + action = createAction(); + action->setVisible(false); + connect(action, SIGNAL(triggered()), SLOT(openRecentFile())); + } + + m_actionRotateAnticlockwise = createAction(tr("Rotate Anticlockwise")); + setIcon(m_actionRotateAnticlockwise, "pentobi-rotate-left"); + connect(m_actionRotateAnticlockwise, SIGNAL(triggered()), + SLOT(rotateAnticlockwise())); + + m_actionRotateClockwise = createAction(tr("Rotate Clockwise")); + setIcon(m_actionRotateClockwise, "pentobi-rotate-right"); + connect(m_actionRotateClockwise, SIGNAL(triggered()), + SLOT(rotateClockwise())); + + m_actionQuit = createAction(tr("&Quit")); + m_actionQuit->setShortcut(QKeySequence::Quit); + connect(m_actionQuit, SIGNAL(triggered()), SLOT(close())); + + m_actionSave = createAction(tr("&Save")); + m_actionSave->setShortcut(QKeySequence::Save); + connect(m_actionSave, SIGNAL(triggered()), SLOT(save())); + + m_actionSaveAs = createAction(tr("Save &As...")); + m_actionSaveAs->setShortcut(QKeySequence::SaveAs); + connect(m_actionSaveAs, SIGNAL(triggered()), SLOT(saveAs())); + + m_actionNextColor = createAction(tr("Next &Color")); + connect(m_actionNextColor, SIGNAL(triggered()), SLOT(nextColor())); + + for (auto name : { "1", "2", "A", "C", "E", "F", "G", "H", "I", "J", "L", + "N", "O", "P", "S", "T", "U", "V", "W", "X", "Y", "Z" }) + { + auto action = createAction(); + action->setData(name); + action->setShortcut(QString(name)); + connect(action, SIGNAL(triggered()), SLOT(selectNamedPiece())); + } + + m_actionSetupMode = createAction(tr("S&etup Mode")); + m_actionSetupMode->setCheckable(true); + connect(m_actionSetupMode, SIGNAL(triggered(bool)), SLOT(setupMode(bool))); + + m_actionShowComment = createAction(tr("&Comment")); + m_actionShowComment->setCheckable(true); + m_actionShowComment->setShortcut(QString("Ctrl+T")); + connect(m_actionShowComment, SIGNAL(triggered(bool)), + SLOT(showComment(bool))); + + m_actionRating = createAction(tr("&Rating")); + m_actionRating->setShortcut(QString("F7")); + connect(m_actionRating, SIGNAL(triggered()), SLOT(showRating())); + + m_actionToolBarNoText = createAction(tr("&No Text")); + m_actionToolBarNoText->setActionGroup(groupToolBarText); + m_actionToolBarNoText->setCheckable(true); + connect(m_actionToolBarNoText, SIGNAL(triggered(bool)), + SLOT(toolBarNoText(bool))); + + m_actionToolBarTextBesideIcons = createAction(tr("Text &Beside Icons")); + m_actionToolBarTextBesideIcons->setActionGroup(groupToolBarText); + m_actionToolBarTextBesideIcons->setCheckable(true); + connect(m_actionToolBarTextBesideIcons, SIGNAL(triggered(bool)), + SLOT(toolBarTextBesideIcons(bool))); + + m_actionToolBarTextBelowIcons = createAction(tr("Text Bel&ow Icons")); + m_actionToolBarTextBelowIcons->setActionGroup(groupToolBarText); + m_actionToolBarTextBelowIcons->setCheckable(true); + connect(m_actionToolBarTextBelowIcons, SIGNAL(triggered(bool)), + SLOT(toolBarTextBelowIcons(bool))); + + m_actionToolBarTextOnly = createAction(tr("&Text Only")); + m_actionToolBarTextOnly->setActionGroup(groupToolBarText); + m_actionToolBarTextOnly->setCheckable(true); + connect(m_actionToolBarTextOnly, SIGNAL(triggered(bool)), + SLOT(toolBarTextOnly(bool))); + + m_actionToolBarTextSystem = createAction(tr("&System Default")); + m_actionToolBarTextSystem->setActionGroup(groupToolBarText); + m_actionToolBarTextSystem->setCheckable(true); + connect(m_actionToolBarTextSystem, SIGNAL(triggered(bool)), + SLOT(toolBarTextSystem(bool))); + + m_actionTruncate = createAction(tr("&Truncate")); + connect(m_actionTruncate, SIGNAL(triggered()), SLOT(truncate())); + + m_actionTruncateChildren = createAction(tr("Truncate C&hildren")); + connect(m_actionTruncateChildren, SIGNAL(triggered()), + SLOT(truncateChildren())); + + m_actionShowToolbar = createAction(tr("&Toolbar")); + m_actionShowToolbar->setCheckable(true); + connect(m_actionShowToolbar, SIGNAL(triggered(bool)), + SLOT(showToolbar(bool))); + + m_actionShowVariations = createAction(tr("Show &Variations")); + m_actionShowVariations->setCheckable(true); + connect(m_actionShowVariations, SIGNAL(triggered(bool)), + SLOT(showVariations(bool))); + + m_actionUndo = createAction(tr("&Undo Move")); + setIcon(m_actionUndo, "pentobi-undo"); + connect(m_actionUndo, SIGNAL(triggered()), SLOT(undo())); + + m_actionVariantCallisto2 = + createActionVariant(Variant::callisto_2, + tr("Callisto (&2 Players)")); + m_actionVariantCallisto3 = + createActionVariant(Variant::callisto_3, + tr("Callisto (&3 Players)")); + m_actionVariantCallisto = + createActionVariant(Variant::callisto, + tr("Callisto (&4 Players)")); + m_actionVariantClassic2 = + createActionVariant(Variant::classic_2, + tr("Classic (&2 Players)")); + m_actionVariantClassic3 = + createActionVariant(Variant::classic_3, + tr("Classic (&3 Players)")); + m_actionVariantClassic = + createActionVariant(Variant::classic, tr("Classic (&4 Players)")); + m_actionVariantDuo = createActionVariant(Variant::duo, tr("&Duo")); + m_actionVariantJunior = + createActionVariant(Variant::junior, tr("J&unior")); + m_actionVariantTrigon2 = + createActionVariant(Variant::trigon_2, tr("Trigon (&2 Players)")); + m_actionVariantTrigon3 = + createActionVariant(Variant::trigon_3, tr("Trigon (&3 Players)")); + m_actionVariantTrigon = + createActionVariant(Variant::trigon, tr("Trigon (&4 Players)")); + m_actionVariantNexos2 = + createActionVariant(Variant::nexos_2, tr("Nexos (&2 Players)")); + m_actionVariantNexos = + createActionVariant(Variant::nexos, tr("Nexos (&4 Players)")); + + m_actionVeryBadMove = createAction(tr("V&ery Bad")); + m_actionVeryBadMove->setActionGroup(groupMoveAnnotation); + m_actionVeryBadMove->setCheckable(true); + connect(m_actionVeryBadMove, SIGNAL(triggered(bool)), + SLOT(veryBadMove(bool))); + + m_actionVeryGoodMove = createAction(tr("&Very Good")); + m_actionVeryGoodMove->setActionGroup(groupMoveAnnotation); + m_actionVeryGoodMove->setCheckable(true); + connect(m_actionVeryGoodMove, SIGNAL(triggered(bool)), + SLOT(veryGoodMove(bool))); +} + +QAction* MainWindow::createActionVariant(Variant variant, const QString& text) +{ + auto action = createAction(text); + action->setCheckable(true); + action->setActionGroup(m_actionGroupVariant); + action->setData(static_cast(variant)); + connect(action, SIGNAL(triggered(bool)), SLOT(variantTriggered(bool))); + return action; +} + +QWidget* MainWindow::createCentralWidget() +{ + auto widget = new QWidget; + // We add spacing around and between the two panels using streches (such + // that the spacing grows with the window size) + auto outerLayout = new QVBoxLayout; + widget->setLayout(outerLayout); + auto innerLayout = new QHBoxLayout; + outerLayout->addStretch(1); + outerLayout->addLayout(innerLayout, 100); + outerLayout->addStretch(1); + innerLayout->addStretch(1); + innerLayout->addWidget(createLeftPanel(), 60); + innerLayout->addStretch(1); + innerLayout->addLayout(createRightPanel(), 40); + innerLayout->addStretch(1); + // The central widget doesn't do anything with the focus right now, but we + // allow it to receive the focus such that the user can switch away the + // focus from the comment field and its blinking cursor. + widget->setFocusPolicy(Qt::StrongFocus); + return widget; +} + +QWidget* MainWindow::createLeftPanel() +{ + m_splitter = new QSplitter(Qt::Vertical); + m_guiBoard = new GuiBoard(nullptr, m_bd); + m_splitter->addWidget(m_guiBoard); + m_comment = new QPlainTextEdit; + m_comment->setTabChangesFocus(true); + connect(m_comment, SIGNAL(textChanged()), SLOT(commentChanged())); + m_splitter->addWidget(m_comment); + m_splitter->setStretchFactor(0, 85); + m_splitter->setStretchFactor(1, 15); + m_splitter->setCollapsible(0, false); + m_splitter->setCollapsible(1, false); + return m_splitter; +} + +void MainWindow::createMenu() +{ + auto menuGame = menuBar()->addMenu(tr("&Game")); + menuGame->addAction(m_actionNew); + menuGame->addAction(m_actionRatedGame); + menuGame->addSeparator(); + m_menuVariant = menuGame->addMenu(tr("Game &Variant")); + auto menuClassic = m_menuVariant->addMenu(tr("&Classic")); + menuClassic->addAction(m_actionVariantClassic2); + menuClassic->addAction(m_actionVariantClassic3); + menuClassic->addAction(m_actionVariantClassic); + m_menuVariant->addAction(m_actionVariantDuo); + m_menuVariant->addAction(m_actionVariantJunior); + auto menuTrigon = m_menuVariant->addMenu(tr("&Trigon")); + menuTrigon->addAction(m_actionVariantTrigon2); + menuTrigon->addAction(m_actionVariantTrigon3); + menuTrigon->addAction(m_actionVariantTrigon); + auto menuNexos = m_menuVariant->addMenu(tr("&Nexos")); + menuNexos->addAction(m_actionVariantNexos2); + menuNexos->addAction(m_actionVariantNexos); + auto menuCallisto = m_menuVariant->addMenu(tr("C&allisto")); + menuCallisto->addAction(m_actionVariantCallisto2); + menuCallisto->addAction(m_actionVariantCallisto3); + menuCallisto->addAction(m_actionVariantCallisto); + menuGame->addAction(m_actionGameInfo); + menuGame->addSeparator(); + menuGame->addAction(m_actionUndo); + menuGame->addAction(m_actionFindMove); + menuGame->addSeparator(); + menuGame->addAction(m_actionOpen); + m_menuOpenRecent = menuGame->addMenu(tr("Open R&ecent")); + for (auto& action : m_actionRecentFile) + m_menuOpenRecent->addAction(action); + menuGame->addSeparator(); + menuGame->addAction(m_actionSave); + menuGame->addAction(m_actionSaveAs); + m_menuExport = menuGame->addMenu(tr("E&xport")); + m_menuExport->addAction(m_actionExportImage); + m_menuExport->addAction(m_actionExportAsciiArt); + menuGame->addSeparator(); + menuGame->addAction(m_actionQuit); + + auto menuGo = menuBar()->addMenu(tr("G&o")); + menuGo->addAction(m_actionBeginning); + menuGo->addAction(m_actionBackward); + menuGo->addAction(m_actionForward); + menuGo->addAction(m_actionEnd); + menuGo->addSeparator(); + menuGo->addAction(m_actionNextVariation); + menuGo->addAction(m_actionPreviousVariation); + menuGo->addSeparator(); + menuGo->addAction(m_actionGotoMove); + menuGo->addAction(m_actionBackToMainVariation); + menuGo->addAction(m_actionBeginningOfBranch); + menuGo->addSeparator(); + menuGo->addAction(m_actionFindNextComment); + + auto menuEdit = menuBar()->addMenu(tr("&Edit")); + m_menuMoveAnnotation = menuEdit->addMenu(tr("&Move Annotation")); + m_menuMoveAnnotation->addAction(m_actionNoMoveAnnotation); + m_menuMoveAnnotation->addAction(m_actionVeryGoodMove); + m_menuMoveAnnotation->addAction(m_actionGoodMove); + m_menuMoveAnnotation->addAction(m_actionInterestingMove); + m_menuMoveAnnotation->addAction(m_actionDoubtfulMove); + m_menuMoveAnnotation->addAction(m_actionBadMove); + m_menuMoveAnnotation->addAction(m_actionVeryBadMove); + menuEdit->addSeparator(); + menuEdit->addAction(m_actionMakeMainVariation); + menuEdit->addAction(m_actionMoveUpVariation); + menuEdit->addAction(m_actionMoveDownVariation); + menuEdit->addSeparator(); + menuEdit->addAction(m_actionDeleteAllVariations); + menuEdit->addAction(m_actionTruncate); + menuEdit->addAction(m_actionTruncateChildren); + menuEdit->addAction(m_actionKeepOnlyPosition); + menuEdit->addAction(m_actionKeepOnlySubtree); + menuEdit->addSeparator(); + menuEdit->addAction(m_actionSetupMode); + menuEdit->addAction(m_actionNextColor); + + auto menuView = menuBar()->addMenu(tr("&View")); + menuView->addAction(m_actionShowToolbar); + m_menuToolBarText = menuView->addMenu(tr("Toolbar T&ext")); + m_menuToolBarText->addAction(m_actionToolBarNoText); + m_menuToolBarText->addAction(m_actionToolBarTextBesideIcons); + m_menuToolBarText->addAction(m_actionToolBarTextBelowIcons); + m_menuToolBarText->addAction(m_actionToolBarTextOnly); + m_menuToolBarText->addAction(m_actionToolBarTextSystem); + menuView->addAction(m_actionShowComment); + menuView->addSeparator(); + auto menuMoveNumbers = menuView->addMenu(tr("&Move Marking")); + menuMoveNumbers->addAction(m_actionMoveMarkingLastDot); + menuMoveNumbers->addAction(m_actionMoveMarkingLastNumber); + menuMoveNumbers->addAction(m_actionMoveMarkingAllNumber); + menuMoveNumbers->addAction(m_actionMoveMarkingNone); + menuView->addAction(m_actionCoordinates); + menuView->addAction(m_actionShowVariations); + menuView->addSeparator(); + menuView->addAction(m_actionFullscreen); + + auto menuComputer = menuBar()->addMenu(tr("&Computer")); + menuComputer->addAction(m_actionComputerColors); + menuComputer->addAction(m_actionPlay); + menuComputer->addSeparator(); + menuComputer->addAction(m_actionPlaySingleMove); + menuComputer->addAction(m_actionInterrupt); + menuComputer->addSeparator(); + m_menuLevel = menuComputer->addMenu(QString()); + m_menuLevel->addActions(m_actionGroupLevel->actions()); + + auto menuTools = menuBar()->addMenu(tr("&Tools")); + menuTools->addAction(m_actionRating); + menuTools->addAction(m_actionAnalyzeGame); + + auto menuHelp = menuBar()->addMenu(tr("&Help")); + menuHelp->addAction(m_actionHelp); + menuHelp->addAction(m_actionAbout); +} + +QLayout* MainWindow::createOrientationButtonBoxLeft() +{ + auto outerLayout = new QVBoxLayout; + auto layout = new QGridLayout; + layout->addWidget(createOBoxToolButton(m_actionRotateAnticlockwise), 0, 0); + layout->addWidget(createOBoxToolButton(m_actionRotateClockwise), 0, 1); + layout->addWidget(createOBoxToolButton(m_actionFlipHorizontally), 1, 0); + layout->addWidget(createOBoxToolButton(m_actionFlipVertically), 1, 1); + outerLayout->addStretch(); + outerLayout->addLayout(layout); + outerLayout->addStretch(); + return outerLayout; +} + +QLayout* MainWindow::createOrientationButtonBoxRight() +{ + auto outerLayout = new QVBoxLayout; + auto layout = new QGridLayout; + layout->addWidget(createOBoxToolButton(m_actionPreviousPiece), 0, 0); + layout->addWidget(createOBoxToolButton(m_actionNextPiece), 0, 1); + layout->addWidget(createOBoxToolButton(m_actionClearPiece), 1, 0, + 1, 2, Qt::AlignHCenter); + outerLayout->addStretch(); + outerLayout->addLayout(layout); + outerLayout->addStretch(); + return outerLayout; +} + +QLayout* MainWindow::createOrientationSelector() +{ + auto layout = new QHBoxLayout; + layout->addStretch(); + layout->addLayout(createOrientationButtonBoxLeft()); + layout->addSpacing(8); + m_orientationDisplay = new OrientationDisplay(nullptr, m_bd); + connect(m_orientationDisplay, SIGNAL(colorClicked(Color)), + SLOT(orientationDisplayColorClicked(Color))); + m_orientationDisplay->setSizePolicy(QSizePolicy::MinimumExpanding, + QSizePolicy::MinimumExpanding); + layout->addWidget(m_orientationDisplay); + layout->addSpacing(8); + layout->addLayout(createOrientationButtonBoxRight()); + layout->addStretch(); + return layout; +} + +QLayout* MainWindow::createRightPanel() +{ + auto layout = new QVBoxLayout; + layout->addLayout(createOrientationSelector(), 20); + m_scoreDisplay = new ScoreDisplay; + layout->addWidget(m_scoreDisplay, 5); + auto pieceSelectorLayout = new SameHeightLayout; + layout->addLayout(pieceSelectorLayout, 80); + for (Color c : Color::Range(Color::range)) + { + m_pieceSelector[c] = new PieceSelector(nullptr, m_bd, c); + connect(m_pieceSelector[c], + SIGNAL(pieceSelected(Color,Piece,const Transform*)), + SLOT(selectPiece(Color,Piece,const Transform*))); + pieceSelectorLayout->addWidget(m_pieceSelector[c]); + } + return layout; +} + +void MainWindow::deleteAllVariations() +{ + QMessageBox msgBox(this); + initQuestion(msgBox, tr("Delete all variations?"), + tr("All variations but the main variation will be" + " removed from the game tree.")); + auto deleteButton = + msgBox.addButton(tr("Delete Variations"), QMessageBox::DestructiveRole); + auto cancelButton = msgBox.addButton(QMessageBox::Cancel); + msgBox.setDefaultButton(cancelButton); + msgBox.exec(); + if (msgBox.clickedButton() != deleteButton) + return; + bool currentNodeChanges = ! is_main_variation(m_game.get_current()); + if (currentNodeChanges) + cancelThread(); + m_game.delete_all_variations(); + updateWindow(currentNodeChanges); +} + +void MainWindow::doubtfulMove(bool checked) +{ + if (! checked) + return; + m_game.set_doubtful_move(); + updateWindow(false); +} + +void MainWindow::createToolBar() +{ + auto toolBar = new QToolBar; + toolBar->setMovable(false); + toolBar->setContextMenuPolicy(Qt::PreventContextMenu); + toolBar->setToolButtonStyle(Qt::ToolButtonFollowStyle); + toolBar->addAction(m_actionNew); + toolBar->addAction(m_actionRatedGame); + toolBar->addAction(m_actionUndo); + toolBar->addSeparator(); + toolBar->addAction(m_actionComputerColors); + toolBar->addAction(m_actionPlay); + toolBar->addSeparator(); + toolBar->addAction(m_actionBeginning); + toolBar->addAction(m_actionBackward); + toolBar->addAction(m_actionForward); + toolBar->addAction(m_actionEnd); + toolBar->addSeparator(); + toolBar->addAction(m_actionNextVariation); + toolBar->addAction(m_actionPreviousVariation); + // Is this the right way to enable autorepeat buttons? Using + // QAction::autoRepeat applies only to keyboard and adding a QToolButton + // with QToolBar::addWidget() makes the tool button not respect the + // toolButtonStyle. + for (auto button : toolBar->findChildren()) + { + auto action = button->defaultAction(); + if (action == m_actionBackward || action == m_actionForward) + button->setAutoRepeat(true); + } + addToolBar(toolBar); + initToolBarText(toolBar); + QSettings settings; + auto toolBarText = settings.value("toolbar_text", "system").toString(); + if (toolBarText == "no_text") + m_actionToolBarNoText->setChecked(true); + else if (toolBarText == "beside_icons") + m_actionToolBarTextBesideIcons->setChecked(true); + else if (toolBarText == "below_icons") + m_actionToolBarTextBelowIcons->setChecked(true); + else if (toolBarText == "text_only") + m_actionToolBarTextOnly->setChecked(true); + else + m_actionToolBarTextSystem->setChecked(true); +} + +void MainWindow::deleteAutoSaveFile() +{ + QString autoSaveFile = getAutoSaveFile(); + QFile file(autoSaveFile); + if (file.exists() && ! file.remove()) + showError(tr("Could not delete %1").arg(autoSaveFile)); +} + +void MainWindow::enablePieceSelector(Color c) +{ + for (Color i : m_bd.get_colors()) + { + m_pieceSelector[i]->checkUpdate(); + m_pieceSelector[i]->setEnabled(i == c); + } +} + +void MainWindow::end() +{ + gotoNode(get_last_node(m_game.get_current())); +} + +bool MainWindow::eventFilter(QObject* object, QEvent* event) +{ + // Empty status tips can clear the status bar if the mouse goes over a + // menu. We don't want that because it deletes our "computer is thinking" + // message. This still happens with Qt 5.6 on some platforms. + if (event->type() == QEvent::StatusTip) + return true; + return QMainWindow::eventFilter(object, event); +} + +void MainWindow::exportAsciiArt() +{ + QString file = QFileDialog::getSaveFileName(this, "", getLastDir(), + tr("Text files (*.txt);;All files (*)")); + if (file.isEmpty()) + return; + rememberDir(file); + ofstream out(file.toLocal8Bit().constData()); + m_bd.write(out, false); + if (! out) + showError(QString::fromLocal8Bit(strerror(errno))); +} + +void MainWindow::exportImage() +{ + QSettings settings; + auto size = settings.value("export_image_size", 420).toInt(); + QInputDialog dialog(this); + dialog.setWindowFlags(dialog.windowFlags() + & ~Qt::WindowContextHelpButtonHint); + dialog.setWindowTitle(tr("Export Image")); + dialog.setLabelText(tr("Image size:")); + dialog.setInputMode(QInputDialog::IntInput); + dialog.setIntRange(0, 2147483647); + dialog.setIntStep(40); + dialog.setIntValue(size); + if (! dialog.exec()) + return; + size = dialog.intValue(); + settings.setValue("export_image_size", size); + bool coordinates = m_actionCoordinates->isChecked(); + BoardPainter boardPainter; + boardPainter.setCoordinates(coordinates); + boardPainter.setCoordinateColor(QColor(100, 100, 100)); + QImage image(size, size, QImage::Format_ARGB32); + image.fill(Qt::transparent); + QPainter painter; + painter.begin(&image); + if (coordinates) + painter.fillRect(0, 0, size, size, QColor(216, 216, 216)); + boardPainter.paintEmptyBoard(painter, size, size, m_bd.get_variant(), + m_bd.get_geometry()); + Grid pieceId; + if (m_bd.get_board_type() == BoardType::nexos) + { + pieceId.fill(0, m_bd.get_geometry()); + unsigned n = 0; + for (Color c : m_bd.get_colors()) + for (Move mv : m_bd.get_setup().placements[c]) + { + ++n; + for (Point p : m_bd.get_move_points(mv)) + pieceId[p] = n; + } + for (auto mv : m_bd.get_moves()) + { + ++n; + for (Point p : m_bd.get_move_points(mv.move)) + pieceId[p] = n; + } + } + boardPainter.paintPieces(painter, m_bd.get_point_state(), pieceId, + &m_guiBoard->getLabels()); + painter.end(); + QString file; + while (true) + { + file = QFileDialog::getSaveFileName(this, file, getLastDir()); + if (file.isEmpty()) + break; + rememberDir(file); + QImageWriter writer(file); + if (writer.write(image)) + break; + else + showError(writer.errorString()); + } +} + +void MainWindow::findMove() +{ + if (m_bd.is_game_over()) + return; + if (! m_legalMoves) + m_legalMoves.reset(new MoveList); + Color to_play = m_bd.get_to_play(); + if (m_legalMoves->empty()) + { + if (! m_marker) + m_marker.reset(new MoveMarker); + m_bd.gen_moves(to_play, *m_marker, *m_legalMoves); + m_marker->clear(*m_legalMoves); + sort(m_legalMoves->begin(), m_legalMoves->end(), + [&](Move mv1, Move mv2) { + return getHeuristic(m_bd, mv1) > getHeuristic(m_bd, mv2); + }); + } + if (m_legalMoves->empty()) + return; + if (m_legalMoveIndex >= m_legalMoves->size()) + m_legalMoveIndex = 0; + auto mv = (*m_legalMoves)[m_legalMoveIndex]; + selectPiece(to_play, m_bd.get_move_piece(mv)); + m_guiBoard->showMove(to_play, mv); + ++m_legalMoveIndex; +} + +void MainWindow::findNextComment() +{ + auto& root = m_game.get_root(); + auto& current = m_game.get_current(); + auto node = find_next_comment(current); + if (! node && ¤t != &root) + { + QMessageBox msgBox(this); + initQuestion(msgBox, tr("The end of the tree was reached."), + tr("Continue the search from the start of the tree?")); + auto continueButton = + msgBox.addButton(tr("Continue From Start"), + QMessageBox::AcceptRole); + msgBox.addButton(QMessageBox::Cancel); + msgBox.setDefaultButton(continueButton); + msgBox.exec(); + if (msgBox.clickedButton() == continueButton) + { + node = &root; + if (! has_comment(*node)) + node = find_next_comment(*node); + } + else + return; + } + if (! node) + { + showInfo(tr("No comment found")); + return; + } + showComment(true); + gotoNode(*node); +} + +void MainWindow::flipHorizontally() +{ + Piece piece = m_guiBoard->getSelectedPiece(); + if (piece.is_null()) + return; + auto transform = m_guiBoard->getSelectedPieceTransform(); + transform = m_bd.get_transforms().get_mirrored_horizontally(transform); + transform = m_bd.get_piece_info(piece).get_equivalent_transform(transform); + m_guiBoard->setSelectedPieceTransform(transform); + m_orientationDisplay->setSelectedPieceTransform(transform); +} + +void MainWindow::flipVertically() +{ + Piece piece = m_guiBoard->getSelectedPiece(); + if (piece.is_null()) + return; + auto transform = m_guiBoard->getSelectedPieceTransform(); + transform = m_bd.get_transforms().get_mirrored_vertically(transform); + transform = m_bd.get_piece_info(piece).get_equivalent_transform(transform); + m_guiBoard->setSelectedPieceTransform(transform); + m_orientationDisplay->setSelectedPieceTransform(transform); +} + +void MainWindow::forward() +{ + gotoNode(m_game.get_current().get_first_child_or_null()); +} + +void MainWindow::fullscreen() +{ + if (isFullScreen()) + { + // If F11 is pressed in fullscreen, we switch to normal + leaveFullscreen(); + return; + } + QSettings settings; + menuBar()->hide(); + findChild()->hide(); + settings.setValue("geometry", saveGeometry()); + m_wasMaximized = isMaximized(); + showFullScreen(); + if (! m_leaveFullscreenButton) + m_leaveFullscreenButton = + new LeaveFullscreenButton(this, m_actionLeaveFullscreen); + m_leaveFullscreenButton->showButton(); +} + +void MainWindow::gameInfo() +{ + GameInfoDialog dialog(this, m_game); + dialog.exec(); + updateWindow(false); +} + +void MainWindow::gameOver() +{ + auto variant = m_bd.get_variant(); + auto nuColors = get_nu_colors(variant); + auto nuPlayers = get_nu_players(variant); + bool breakTies = (m_bd.get_piece_set() == PieceSet::callisto); + QString info; + if (nuColors == 2) + { + auto score = m_bd.get_score_twocolor(Color(0)); + if (score == 1) + info = tr("Blue wins with 1 point."); + else if (score > 0) + info = tr("Blue wins with %1 points.").arg(score); + else if (score == -1) + info = tr("Green wins with 1 point."); + else if (score < 0) + info = tr("Green wins with %1 points.").arg(-score); + else if (breakTies) + info = tr("Green wins (tie resolved)."); + else + info = tr("The game ends in a tie."); + } + else if (nuPlayers == 2) + { + LIBBOARDGAME_ASSERT(nuColors == 4); + auto score = m_bd.get_score_multicolor(Color(0)); + if (score == 1) + info = tr("Blue/Red wins with 1 point."); + else if (score > 0) + info = tr("Blue/Red wins with %1 points.").arg(score); + else if (score == -1) + info = tr("Yellow/Green wins with 1 point."); + else if (score < 0) + info = tr("Yellow/Green wins with %1 points.").arg(-score); + else if (breakTies) + info = tr("Yellow/Green wins (tie resolved)."); + else + info = tr("The game ends in a tie."); + } + else if (nuPlayers == 3) + { + auto blue = m_bd.get_points(Color(0)); + auto yellow = m_bd.get_points(Color(1)); + auto red = m_bd.get_points(Color(2)); + auto maxPoints = max(blue, max(yellow, red)); + if (breakTies && red == maxPoints + && (blue == maxPoints || yellow == maxPoints)) + info = tr("Red wins (tie resolved)."); + else if (breakTies && yellow == maxPoints && blue == maxPoints) + info = tr("Yellow wins (tie resolved)."); + else if (blue == yellow && yellow == red) + info = tr("The game ends in a tie between all colors."); + else if (blue == maxPoints && blue == yellow) + info = tr("The game ends in a tie between Blue and Yellow."); + else if (blue == maxPoints && blue == red) + info = tr("The game ends in a tie between Blue and Red."); + else if (yellow == maxPoints && yellow == red) + info = tr("The game ends in a tie between Yellow and Red."); + else if (blue == maxPoints) + info = tr("Blue wins."); + else if (yellow == maxPoints) + info = tr("Yellow wins."); + else + info = tr("Red wins."); + } + else + { + LIBBOARDGAME_ASSERT(nuPlayers == 4); + auto blue = m_bd.get_points(Color(0)); + auto yellow = m_bd.get_points(Color(1)); + auto red = m_bd.get_points(Color(2)); + auto green = m_bd.get_points(Color(3)); + auto maxPoints = max(blue, max(yellow, max(red, green))); + if (breakTies && green == maxPoints + && (red == maxPoints || blue == maxPoints + || yellow == maxPoints)) + info = tr("Green wins (tie resolved)."); + else if (breakTies && red == maxPoints + && (blue == maxPoints || yellow == maxPoints)) + info = tr("Red wins (tie resolved)."); + else if (breakTies && yellow == maxPoints && blue == maxPoints) + info = tr("Yellow wins (tie resolved)."); + else if (blue == yellow && yellow == red && red == green) + info = tr("The game ends in a tie between all colors."); + else if (blue == maxPoints && blue == yellow && yellow == red) + info = tr("The game ends in a tie between Blue, Yellow and Red."); + else if (blue == maxPoints && blue == yellow && yellow == green) + info = + tr("The game ends in a tie between Blue, Yellow and Green."); + else if (blue == maxPoints && blue == red && red == green) + info = tr("The game ends in a tie between Blue, Red and Green."); + else if (yellow == maxPoints && yellow == red && red == green) + info = tr("The game ends in a tie between Yellow, Red and Green."); + else if (blue == maxPoints && blue == yellow) + info = tr("The game ends in a tie between Blue and Yellow."); + else if (blue == maxPoints && blue == red) + info = tr("The game ends in a tie between Blue and Red."); + else if (blue == maxPoints && blue == green) + info = tr("The game ends in a tie between Blue and Green."); + else if (yellow == maxPoints && yellow == red) + info = tr("The game ends in a tie between Yellow and Red."); + else if (yellow == maxPoints && yellow == green) + info = tr("The game ends in a tie between Yellow and Green."); + else if (red == maxPoints && red == green) + info = tr("The game ends in a tie between Red and Green."); + else if (blue == maxPoints) + info = tr("Blue wins."); + else if (yellow == maxPoints) + info = tr("Yellow wins."); + else if (red == maxPoints) + info = tr("Red wins."); + else + info = tr("Green wins."); + } + if (m_isRated) + { + QString detailText; + int oldRating = m_history->getRating().to_int(); + unsigned place; + bool isPlaceShared; + m_bd.get_place(m_ratedGameColor, place, isPlaceShared); + float gameResult; + if (place == 0 && !isPlaceShared) + gameResult = 1; + else if (place == 0 && isPlaceShared) + gameResult = 0.5; + else + gameResult = 0; + unsigned nuOpp = get_nu_players(variant) - 1; + Rating oppRating = m_player->get_rating(variant); + QString date = QString(PentobiTree::get_date_today().c_str()); + m_history->addGame(gameResult, oppRating, nuOpp, m_ratedGameColor, + gameResult, date, m_level, m_game.get_tree()); + if (m_ratingDialog) + m_ratingDialog->updateContent(); + int newRating = m_history->getRating().to_int(); + if (newRating > oldRating) + detailText = tr("Your rating has increased from %1 to %2.") + .arg(QString::number(oldRating), QString::number(newRating)); + else if (newRating == oldRating) + detailText = tr("Your rating stays at %1.").arg(oldRating); + else + detailText = + tr("Your rating has decreased from %1 to %2.") + .arg(QString::number(oldRating), QString::number(newRating)); + setRated(false); + QSettings settings; + auto key = QString("next_rated_random_") + to_string_id(variant); + settings.remove(key); + QMessageBox msgBox(this); + Util::setNoTitle(msgBox); + msgBox.setIcon(QMessageBox::Information); + msgBox.setText(info); + msgBox.setInformativeText(detailText); + auto showRatingButton = + msgBox.addButton(tr("Show &Rating"), QMessageBox::AcceptRole); + msgBox.addButton(QMessageBox::Close); + msgBox.setDefaultButton(showRatingButton); + msgBox.exec(); + auto result = msgBox.clickedButton(); + if (result == showRatingButton) + showRating(); + } + else + showInfo(info); +} + +void MainWindow::genMove(bool playSingleMove) +{ + cancelThread(); + ++m_genMoveId; + setCursor(QCursor(Qt::BusyCursor)); + m_actionNextPiece->setEnabled(false); + m_actionPreviousPiece->setEnabled(false); + m_actionPlay->setEnabled(false); + m_actionPlaySingleMove->setEnabled(false); + m_actionInterrupt->setEnabled(true); + showStatus(tr("Computer is thinking...")); + clearPiece(); + clear_abort(); + m_lastRemainingSeconds = 0; + m_lastRemainingMinutes = 0; + m_player->set_level(m_level); + QFuture future = + QtConcurrent::run(this, &MainWindow::asyncGenMove, m_bd.get_to_play(), + m_genMoveId, playSingleMove); + m_genMoveWatcher.setFuture(future); + m_isGenMoveRunning = true; +} + +void MainWindow::genMoveFinished() +{ + m_actionInterrupt->setEnabled(false); + clearStatus(); + GenMoveResult result = m_genMoveWatcher.future().result(); + if (result.genMoveId != m_genMoveId) + { + // Callback from a move generation canceled with cancelThread() + return; + } + LIBBOARDGAME_ASSERT(m_isGenMoveRunning); + m_isGenMoveRunning = false; + setCursor(QCursor(Qt::ArrowCursor)); + if (get_abort() && computerPlaysAll()) + m_computerColors.fill(false); + Color c = result.color; + auto mv = result.move; + if (mv.is_null()) + { + // No need to translate, should never happen if program is correct + showError("Computer failed to generate a move"); + return; + } + if (! m_bd.is_legal(c, mv)) + { + // No need to translate, should never happen if program is correct + showError("Computer generated illegal move"); + return; + } + play(c, mv); + // Call updateWindow() before checkComputerMove() because checkComputerMove + // resets m_lastComputerMovesBegin if computer doesn't play current color + // and updateWindow needs m_lastComputerMovesBegin + updateWindow(true); + if (! result.playSingleMove) + checkComputerMove(); +} + +QString MainWindow::getFilter() const +{ + return tr("Blokus games (*.blksgf);;All files (*)"); +} + +QString MainWindow::getLastDir() +{ + QSettings settings; + auto dir = settings.value("last_dir", "").toString(); + if (dir.isEmpty() || ! QFileInfo::exists(dir)) + dir = QStandardPaths::writableLocation( + QStandardPaths::DocumentsLocation); + return dir; +} + +QString MainWindow::getVersion() const +{ + QString version; +#ifdef VERSION + version = VERSION; +#endif + if (version.isEmpty()) + version = "UNKNOWN"; + return version; +} + +void MainWindow::goodMove(bool checked) +{ + if (! checked) + return; + m_game.set_good_move(); + updateWindow(false); +} + +void MainWindow::gotoMove() +{ + vector nodes; + auto& tree = m_game.get_tree(); + auto node = &m_game.get_current(); + do + { + if (! tree.get_move(*node).is_null()) + nodes.insert(nodes.begin(), node); + node = node->get_parent_or_null(); + } + while (node); + node = m_game.get_current().get_first_child_or_null(); + while (node) + { + if (! tree.get_move(*node).is_null()) + nodes.push_back(node); + node = node->get_first_child_or_null(); + } + int maxMoves = int(nodes.size()); + if (maxMoves == 0) + return; + int defaultValue = m_bd.get_nu_moves(); + if (defaultValue == 0) + defaultValue = maxMoves; + QInputDialog dialog(this); + dialog.setWindowFlags(dialog.windowFlags() + & ~Qt::WindowContextHelpButtonHint); + dialog.setWindowTitle(tr("Go to Move")); + dialog.setLabelText(tr("Move number:")); + dialog.setInputMode(QInputDialog::IntInput); + dialog.setIntRange(1, static_cast(nodes.size())); + dialog.setIntStep(1); + dialog.setIntValue(defaultValue); + if (dialog.exec()) + gotoNode(*nodes[dialog.intValue() - 1]); +} + +void MainWindow::gotoNode(const SgfNode& node) +{ + cancelThread(); + leaveSetupMode(); + try + { + m_game.goto_node(node); + } + catch (const InvalidTree& e) + { + showInvalidFile(m_file, e); + return; + } + if (m_analyzeGameWindow && m_analyzeGameWindow->isVisible()) + m_analyzeGameWindow->analyzeGameWidget + ->setCurrentPosition(m_game, node); + m_autoPlay = false; + updateWindow(true); +} + +void MainWindow::gotoNode(const SgfNode* node) +{ + if (node) + gotoNode(*node); +} + +void MainWindow::gotoPosition(Variant variant, + const vector& moves) +{ + if (m_bd.get_variant() != variant) + return; + auto& tree = m_game.get_tree(); + auto node = &tree.get_root(); + if (tree.has_move_ignore_invalid(*node)) + // Move in root node not supported. + return; + for (ColorMove mv : moves) + { + bool found = false; + for (auto& i : node->get_children()) + if (tree.get_move(i) == mv) + { + found = true; + node = &i; + break; + } + if (! found) + return; + } + gotoNode(*node); +} + +void MainWindow::help() +{ + if (m_helpWindow) + { + m_helpWindow->show(); + m_helpWindow->raise(); + return; + } + QString path = HelpWindow::findMainPage(m_helpDir, "pentobi"); + m_helpWindow = new HelpWindow(nullptr, tr("Pentobi Help"), path); + initToolBarText(m_helpWindow->findChild()); + m_helpWindow->show(); +} + +void MainWindow::initGame() +{ + setRated(false); + if (m_analyzeGameWindow) + { + delete m_analyzeGameWindow; + m_analyzeGameWindow = nullptr; + } + m_game.init(); + m_game.set_charset("UTF-8"); +#ifdef VERSION + m_game.set_application("Pentobi", VERSION); +#else + m_game.set_application("Pentobi"); +#endif + m_game.set_date_today(); + m_game.clear_modified(); + QSettings settings; + if (! settings.value("computer_color_none").toBool()) + { + for (Color c : Color::Range(m_bd.get_nu_nonalt_colors())) + m_computerColors[c] = ! m_bd.is_same_player(c, Color(0)); + m_autoPlay = true; + } + else + { + m_computerColors.fill(false); + m_autoPlay = false; + } + leaveSetupMode(); + m_gameFinished = false; + m_isAutoSaveLoaded = false; + setFile(""); +} + +void MainWindow::initVariantActions() +{ + // Use a temporary const variable to avoid that QList detaches in for loop + const auto actions = m_actionGroupVariant->actions(); + for (auto action : actions) + if (Variant(action->data().toInt()) == m_bd.get_variant()) + { + action->setChecked(true); + return; + } +} + +void MainWindow::initPieceSelectors() +{ + for (Color::IntType i = 0; i < Color::range; ++i) + { + bool isVisible = (i < m_bd.get_nu_colors()); + m_pieceSelector[Color(i)]->setVisible(isVisible); + if (isVisible) + m_pieceSelector[Color(i)]->init(); + } +} + +void MainWindow::interestingMove(bool checked) +{ + if (! checked) + return; + m_game.set_interesting_move(); + updateWindow(false); +} + +void MainWindow::interrupt() +{ + cancelThread(); + m_autoPlay = false; +} + +void MainWindow::interruptPlay() +{ + if (! m_isGenMoveRunning) + return; + set_abort(); + m_autoPlay = false; +} + +bool MainWindow::isComputerToPlay() const +{ + Color to_play = m_bd.get_to_play(); + if (m_game.get_variant() != Variant::classic_3 || to_play != Color(3)) + return m_computerColors[to_play]; + return m_computerColors[Color(m_bd.get_alt_player())]; +} + +void MainWindow::keepOnlyPosition() +{ + QMessageBox msgBox(this); + initQuestion(msgBox, tr("Keep only position?"), + tr("All previous and following moves and variations will" + " be removed from the game tree.")); + auto keepOnlyPositionButton = + msgBox.addButton(tr("Keep Only Position"), + QMessageBox::DestructiveRole); + auto cancelButton = msgBox.addButton(QMessageBox::Cancel); + msgBox.setDefaultButton(cancelButton); + msgBox.exec(); + if (msgBox.clickedButton() != keepOnlyPositionButton) + return; + cancelThread(); + m_game.keep_only_position(); + updateWindow(true); +} + +void MainWindow::keepOnlySubtree() +{ + QMessageBox msgBox(this); + initQuestion(msgBox, tr("Keep only subtree?"), + tr("All previous moves and variations will be removed" + " from the game tree.")); + auto keepOnlySubtreeButton = + msgBox.addButton(tr("Keep Only Subtree"), + QMessageBox::DestructiveRole); + auto cancelButton = msgBox.addButton(QMessageBox::Cancel); + msgBox.setDefaultButton(cancelButton); + msgBox.exec(); + if (msgBox.clickedButton() != keepOnlySubtreeButton) + return; + cancelThread(); + m_game.keep_only_subtree(); + updateWindow(true); +} + +void MainWindow::leaveFullscreen() +{ + if (! isFullScreen()) + return; + QSettings settings; + auto showToolbar = settings.value("toolbar", true).toBool(); + menuBar()->show(); + findChild()->setVisible(showToolbar); + // m_leaveFullscreenButton can be null if the window was put in fullscreen + // mode by a "generic" method by the window manager (e.g. the title bar + // menu on KDE) and not by MainWindow::fullscreen() + if (m_leaveFullscreenButton) + m_leaveFullscreenButton->hideButton(); + // Call showNormal() even if m_wasMaximized otherwise restoring the + // maximized window state does not work correctly on Xfce + showNormal(); + if (m_wasMaximized) + showMaximized(); +} + +void MainWindow::leaveSetupMode() +{ + if (! m_actionSetupMode->isChecked()) + return; + setupMode(false); +} + +void MainWindow::levelTriggered(bool checked) +{ + if (checked) + setLevel(qobject_cast(sender())->data().toUInt()); +} + +void MainWindow::loadHistory() +{ + auto variant = m_game.get_variant(); + if (m_history->getVariant() == variant) + return; + m_history->load(variant); + if (m_ratingDialog) + m_ratingDialog->updateContent(); +} + +void MainWindow::makeMainVariation() +{ + m_game.make_main_variation(); + updateWindow(false); +} + +void MainWindow::moveDownVariation() +{ + m_game.move_down_variation(); + updateWindow(false); +} + +void MainWindow::moveUpVariation() +{ + m_game.move_up_variation(); + updateWindow(false); +} + +void MainWindow::nextColor() +{ + m_game.set_to_play(m_bd.get_next(m_bd.get_to_play())); + auto to_play = m_bd.get_to_play(); + m_orientationDisplay->selectColor(to_play); + clearPiece(); + for (Color c : m_bd.get_colors()) + m_pieceSelector[c]->setEnabled(to_play == c); + if (m_actionSetupMode->isChecked()) + setSetupPlayer(); + updateWindow(false); +} + +void MainWindow::nextPiece() +{ + auto c = m_bd.get_to_play(); + if (m_bd.get_pieces_left(c).empty()) + return; + auto nuUniqPieces = m_bd.get_nu_uniq_pieces(); + Piece::IntType i; + Piece selectedPiece = m_guiBoard->getSelectedPiece(); + if (! selectedPiece.is_null()) + i = static_cast(selectedPiece.to_int() + 1); + else + i = 0; + while (true) + { + if (i >= nuUniqPieces) + i = 0; + if (m_bd.is_piece_left(c, Piece(i))) + break; + ++i; + } + selectPiece(c, Piece(i)); +} + +void MainWindow::nextTransform() +{ + Piece piece = m_guiBoard->getSelectedPiece(); + if (piece.is_null()) + return; + auto transform = m_guiBoard->getSelectedPieceTransform(); + transform = m_bd.get_piece_info(piece).get_next_transform(transform); + m_guiBoard->setSelectedPieceTransform(transform); + m_orientationDisplay->setSelectedPieceTransform(transform); +} + +void MainWindow::nextVariation() +{ + gotoNode(m_game.get_current().get_sibling()); +} + +void MainWindow::newGame() +{ + if (! checkSave()) + return; + cancelThread(); + initGame(); + deleteAutoSaveFile(); + updateWindow(true); +} + +void MainWindow::noMoveAnnotation(bool checked) +{ + if (! checked) + return; + m_game.remove_move_annotation(); + updateWindow(false); +} + +void MainWindow::open() +{ + if (! checkSave()) + return; + QString file = QFileDialog::getOpenFileName(this, tr("Open"), getLastDir(), + getFilter()); + if (file.isEmpty()) + return; + rememberDir(file); + if (open(file)) + rememberFile(file); +} + +bool MainWindow::open(const QString& file, bool isTemporary) +{ + if (file.isEmpty()) + return false; + cancelThread(); + TreeReader reader; + ifstream in(file.toLocal8Bit().constData()); + try + { + reader.read(in); + } + catch (const TreeReader::ReadError& e) + { + if (! in) + { + QString text = + tr("Could not read file '%1'").arg(QFileInfo(file).fileName()); + showError(text, QString::fromLocal8Bit(strerror(errno))); + } + else + { + showInvalidFile(file, e); + } + return false; + } + m_isAutoSaveLoaded = false; + if (! isTemporary) + { + setFile(file); + deleteAutoSaveFile(); + } + if (m_analyzeGameWindow) + { + delete m_analyzeGameWindow; + m_analyzeGameWindow = nullptr; + } + setRated(false); + try + { + auto tree = reader.get_tree_transfer_ownership(); + m_game.init(tree); + if (! libpentobi_base::node_util::has_setup(m_game.get_root())) + m_game.goto_node(get_last_node(m_game.get_root())); + initPieceSelectors(); + } + catch (const InvalidTree& e) + { + showInvalidFile(file, e); + } + m_computerColors.fill(false); + m_autoPlay = false; + leaveSetupMode(); + initVariantActions(); + restoreLevel(m_bd.get_variant()); + updateWindow(true); + loadHistory(); + return true; +} + +void MainWindow::openRecentFile() +{ + auto action = qobject_cast(sender()); + if (action) + openCheckSave(action->data().toString()); +} + +void MainWindow::openCheckSave(const QString& file) +{ + if (checkSave()) + open(file); +} + +void MainWindow::orientationDisplayColorClicked(Color) +{ + if (m_actionSetupMode->isChecked()) + nextColor(); +} + +void MainWindow::placePiece(Color c, Move mv) +{ + cancelThread(); + bool isSetupMode = m_actionSetupMode->isChecked(); + bool isAltColor = + (m_bd.get_variant() == Variant::classic_3 && c.to_int() == 3); + if ((! isAltColor && m_computerColors[c]) + || (isAltColor && m_computerColors[Color(m_bd.get_alt_player())]) + || isSetupMode) + // If the user enters a move previously played by the computer (e.g. + // after undoing moves) then it is unlikely that the user wants to keep + // the computer color settings. + m_computerColors.fill(false); + if (isSetupMode) + { + m_game.add_setup(c, mv); + setSetupPlayer(); + updateWindow(true); + } + else + { + play(c, mv); + updateWindow(true); + checkComputerMove(); + } +} + +void MainWindow::play() +{ + cancelThread(); + leaveSetupMode(); + if (! isComputerToPlay()) + { + m_computerColors.fill(false); + auto c = m_bd.get_to_play(); + if (m_bd.get_variant() == Variant::classic_3 && c == Color(3)) + m_computerColors[Color(m_bd.get_alt_player())] = true; + else + { + m_computerColors[c] = true; + m_computerColors[m_bd.get_second_color(c)] = true; + } + QSettings settings; + settings.setValue("computer_color_none", false); + } + m_autoPlay = true; + genMove(); +} + +void MainWindow::play(Color c, Move mv) +{ + m_game.play(c, mv, false); + m_gameFinished = false; + if (m_bd.is_game_over()) + { + updateWindow(true); + repaint(); + gameOver(); + m_gameFinished = true; + deleteAutoSaveFile(); + return; + } +} + +void MainWindow::playSingleMove() +{ + cancelThread(); + leaveSetupMode(); + m_autoPlay = false; + genMove(true); +} + +void MainWindow::pointClicked(Point p) +{ + // If a piece on the board is clicked on in setup mode, remove it and make + // it the selected piece without changing its orientation. + if (! m_actionSetupMode->isChecked()) + return; + PointState s = m_bd.get_point_state(p); + if (s.is_empty()) + return; + Color c = s.to_color(); + Move mv = m_bd.get_move_at(p); + m_game.remove_setup(c, mv); + setSetupPlayer(); + updateWindow(true); + selectPiece(c, m_bd.get_move_piece(mv), m_bd.find_transform(mv)); + m_guiBoard->setSelectedPiecePoints(mv); +} + +void MainWindow::previousPiece() +{ + auto c = m_bd.get_to_play(); + if (m_bd.get_pieces_left(c).empty()) + return; + auto nuUniqPieces = m_bd.get_nu_uniq_pieces(); + Piece::IntType i; + Piece selectedPiece = m_guiBoard->getSelectedPiece(); + if (! selectedPiece.is_null()) + i = selectedPiece.to_int(); + else + i = 0; + while (true) + { + if (i == 0) + i = static_cast(nuUniqPieces - 1); + else + --i; + if (m_bd.is_piece_left(c, Piece(i))) + break; + } + selectPiece(c, Piece(i)); +} + +void MainWindow::previousTransform() +{ + Piece piece = m_guiBoard->getSelectedPiece(); + if (piece.is_null()) + return; + auto transform = m_guiBoard->getSelectedPieceTransform(); + transform = + m_bd.get_piece_info(piece).get_previous_transform(transform); + m_guiBoard->setSelectedPieceTransform(transform); + m_orientationDisplay->setSelectedPieceTransform(transform); +} + +void MainWindow::previousVariation() +{ + gotoNode(m_game.get_current().get_previous_sibling()); +} + +void MainWindow::ratedGame() +{ + if (! checkSave()) + return; + cancelThread(); + if (m_history->getNuGames() == 0) + { + InitialRatingDialog dialog(this); + if (dialog.exec() != QDialog::Accepted) + return; + m_history->init(Rating(static_cast(dialog.getRating()))); + } + int level; + QSettings settings; + unsigned random; + auto variant = m_bd.get_variant(); + auto key = QString("next_rated_random_") + to_string_id(variant); + if (settings.contains(key)) + random = settings.value(key).toUInt(); + else + { + // RandomGenerator::ResultType may be larger than unsigned + random = static_cast(m_random.generate() % 1000); + settings.setValue(key, random); + } + m_history->getNextRatedGameSettings(m_maxLevel, random, + level, m_ratedGameColor); + QMessageBox msgBox(this); + initQuestion(msgBox, tr("Start rated game?"), + "" + + tr("In this game, you play %1 against Pentobi level %2.") + .arg(getPlayerString(variant, m_ratedGameColor), + QString::number(level))); + auto startGameButton = + msgBox.addButton(tr("&Start Game"), QMessageBox::AcceptRole); + msgBox.addButton(QMessageBox::Cancel); + msgBox.setDefaultButton(startGameButton); + msgBox.exec(); + auto result = msgBox.clickedButton(); + if (result != startGameButton) + return; + setLevel(level); + initGame(); + setFile(""); + setRated(true); + m_computerColors.fill(true); + for (Color c : Color::Range(m_bd.get_nu_nonalt_colors())) + if (m_bd.is_same_player(c, m_ratedGameColor)) + m_computerColors[c] = false; + m_autoPlay = true; + QString computerPlayerName = + //: The first argument is the version of Pentobi + tr("Pentobi %1 (level %2)").arg(getVersion(), QString::number(level)); + string charset = m_game.get_root().get_property("CA", ""); + string computerPlayerNameStdStr = + Util::convertSgfValueFromQString(computerPlayerName, charset); + string humanPlayerNameStdStr = + Util::convertSgfValueFromQString(tr("Human"), charset); + for (Color c : Color::Range(m_bd.get_nu_nonalt_colors())) + if (m_computerColors[c]) + m_game.set_player_name(c, computerPlayerNameStdStr); + else + m_game.set_player_name(c, humanPlayerNameStdStr); + // Setting the player names marks the game as modified but there is nothing + // important that would need to be saved yet + m_game.clear_modified(); + deleteAutoSaveFile(); + updateWindow(true); + checkComputerMove(); +} + +void MainWindow::rememberDir(const QString& file) +{ + if (file.isEmpty()) + return; + QString canonicalFile = file; + QString canonicalFilePath = QFileInfo(file).canonicalFilePath(); + if (! canonicalFilePath.isEmpty()) + canonicalFile = canonicalFilePath; + QFileInfo info(canonicalFile); + QSettings settings; + settings.setValue("last_dir", info.dir().path()); +} + +void MainWindow::rememberFile(const QString& file) +{ + if (file.isEmpty()) + return; + QString canonicalFile = file; + QString canonicalFilePath = QFileInfo(file).canonicalFilePath(); + if (! canonicalFilePath.isEmpty()) + canonicalFile = canonicalFilePath; + QSettings settings; + auto files = settings.value("recent_files").toStringList(); + files.removeAll(canonicalFile); + files.prepend(canonicalFile); + while (files.size() > maxRecentFiles) + files.removeLast(); + settings.setValue("recent_files", files); + settings.sync(); // updateRecentFiles() needs the new settings + updateRecentFiles(); +} + +void MainWindow::restoreLevel(Variant variant) +{ + QSettings settings; + QString key = QString("level_") + to_string_id(variant); + m_level = settings.value(key, 1).toInt(); + if (m_level < 1) + m_level = 1; + if (m_level > m_maxLevel) + m_level = m_maxLevel; + m_actionGroupLevel->actions().at(m_level - 1)->setChecked(true); +} + +void MainWindow::rotateAnticlockwise() +{ + Piece piece = m_guiBoard->getSelectedPiece(); + if (piece.is_null()) + return; + auto transform = m_guiBoard->getSelectedPieceTransform(); + transform = m_bd.get_transforms().get_rotated_anticlockwise(transform); + transform = m_bd.get_piece_info(piece).get_equivalent_transform(transform); + m_guiBoard->setSelectedPieceTransform(transform); + m_orientationDisplay->setSelectedPieceTransform(transform); + updateFlipActions(); +} + +void MainWindow::rotateClockwise() +{ + Piece piece = m_guiBoard->getSelectedPiece(); + if (piece.is_null()) + return; + auto transform = m_guiBoard->getSelectedPieceTransform(); + transform = m_bd.get_transforms().get_rotated_clockwise(transform); + transform = m_bd.get_piece_info(piece).get_equivalent_transform(transform); + m_guiBoard->setSelectedPieceTransform(transform); + m_orientationDisplay->setSelectedPieceTransform(transform); + updateFlipActions(); +} + +void MainWindow::save() +{ + if (m_file.isEmpty()) + saveAs(); + else if (save(m_file)) + { + m_game.clear_modified(); + updateWindow(false); + } +} + +bool MainWindow::save(const QString& file) +{ + if (! writeGame(file.toLocal8Bit().constData())) + { + showError(tr("The file could not be saved."), + /*: Error message if file cannot be saved. %1 is + replaced by the file name, %2 by the error message + of the operating system. */ + tr("%1: %2").arg(file, + QString::fromLocal8Bit(strerror(errno)))); + return false; + } + else + { + Util::removeThumbnail(file); + return true; + } +} + +void MainWindow::saveAs() +{ + QString file = m_file; + if (file.isEmpty()) + { + file = getLastDir(); + file.append(QDir::separator()); + file.append(tr("Untitled Game.blksgf")); + if (QFileInfo::exists(file)) + for (unsigned i = 1; ; ++i) + { + file = getLastDir(); + file.append(QDir::separator()); + file.append(tr("Untitled Game %1.blksgf").arg(i)); + if (! QFileInfo::exists(file)) + break; + } + } + file = QFileDialog::getSaveFileName(this, tr("Save"), file, getFilter()); + if (! file.isEmpty()) + { + rememberDir(file); + if (save(file)) + { + m_game.clear_modified(); + updateWindow(false); + } + setFile(file); + rememberFile(file); + } +} + +void MainWindow::searchCallback(double elapsedSeconds, double remainingSeconds) +{ + // If the search is longer than 10 sec, we show the (maximum) remaining + // time (only during a move generation, ignore search callbacks during + // game analysis) + if (! m_isGenMoveRunning || elapsedSeconds < 10) + return; + QString text; + int seconds = static_cast(ceil(remainingSeconds)); + if (seconds < 90) + { + if (seconds == m_lastRemainingSeconds) + return; + m_lastRemainingSeconds = seconds; + text = + tr("Computer is thinking... (up to %1 seconds remaining)") + .arg(seconds); + } + else + { + int minutes = static_cast(ceil(remainingSeconds / 60)); + if (minutes == m_lastRemainingMinutes) + return; + m_lastRemainingMinutes = minutes; + text = + tr("Computer is thinking... (up to %1 minutes remaining)") + .arg(minutes); + } + QMetaObject::invokeMethod(statusBar(), "showMessage", Q_ARG(QString, text), + Q_ARG(int, 0)); +} + +void MainWindow::selectNamedPiece() +{ + string name(qobject_cast(sender())->data().toString() + .toLocal8Bit().constData()); + auto c = m_bd.get_to_play(); + Board::PiecesLeftList pieces; + for (Piece::IntType i = 0; i < m_bd.get_nu_uniq_pieces(); ++i) + { + Piece piece(i); + if (m_bd.is_piece_left(c, piece) + && m_bd.get_piece_info(piece).get_name().find(name) == 0) + pieces.push_back(piece); + } + if (pieces.empty()) + return; + auto piece = m_guiBoard->getSelectedPiece(); + if (piece.is_null()) + piece = pieces[0]; + else + { + auto pos = std::find(pieces.begin(), pieces.end(), piece); + if (pos == pieces.end()) + piece = pieces[0]; + else + { + ++pos; + if (pos == pieces.end()) + piece = pieces[0]; + else + piece = *pos; + } + } + selectPiece(c, piece); +} + +void MainWindow::selectPiece(Color c, Piece piece) +{ + selectPiece(c, piece, m_bd.get_transforms().get_default()); +} + +void MainWindow::selectPiece(Color c, Piece piece, const Transform* transform) +{ + if (m_isGenMoveRunning + || (m_bd.is_game_over() && ! m_actionSetupMode->isChecked())) + return; + m_game.set_to_play(c); + m_guiBoard->selectPiece(c, piece); + m_guiBoard->setSelectedPieceTransform(transform); + m_orientationDisplay->selectColor(c); + m_orientationDisplay->setSelectedPiece(piece); + m_orientationDisplay->setSelectedPieceTransform(transform); + bool can_rotate = m_bd.get_piece_info(piece).can_rotate(); + m_actionRotateClockwise->setEnabled(can_rotate); + m_actionRotateAnticlockwise->setEnabled(can_rotate); + updateFlipActions(); + m_actionClearPiece->setEnabled(true); +} + +void MainWindow::setCommentText(const QString& text) +{ + m_ignoreCommentTextChanged = true; + m_comment->setPlainText(text); + m_ignoreCommentTextChanged = false; + if (! text.isEmpty()) + m_comment->ensureCursorVisible(); + m_comment->clearFocus(); + updateWindow(false); +} + +void MainWindow::setNoDelay() +{ + m_noDelay = true; +} + +void MainWindow::setVariant(Variant variant) +{ + if (m_bd.get_variant() == variant) + return; + if (! checkSave()) + { + initVariantActions(); + return; + } + cancelThread(); + QSettings settings; + settings.setValue("variant", to_string_id(variant)); + clearPiece(); + m_game.init(variant); + initPieceSelectors(); + newGame(); + loadHistory(); + restoreLevel(variant); +} + +void MainWindow::setFile(const QString& file) +{ + m_file = file; + // Don't use setWindowFilePath() because of QTBUG-16507 + if (m_file.isEmpty()) + setWindowTitle(tr("Pentobi")); + else + setWindowTitle(tr("[*]%1").arg(QFileInfo(m_file).fileName())); +} + +void MainWindow::setLevel(unsigned level) +{ + if (level < 1 || level > m_maxLevel) + return; + m_level = level; + m_actionGroupLevel->actions().at(level - 1)->setChecked(true); + QSettings settings; + settings.setValue(QString("level_") + to_string_id(m_bd.get_variant()), + m_level); +} + +void MainWindow::setMoveMarkingAllNumber(bool checked) +{ + if (! checked) + return; + QSettings settings; + settings.setValue("move_marking", "all_number"); + updateWindow(false); +} + +void MainWindow::setMoveMarkingLastDot(bool checked) +{ + if (! checked) + return; + QSettings settings; + settings.setValue("move_marking", "last_dot"); + updateWindow(false); +} + +void MainWindow::setMoveMarkingLastNumber(bool checked) +{ + if (! checked) + return; + QSettings settings; + settings.setValue("move_marking", "last_number"); + updateWindow(false); +} + +void MainWindow::setMoveMarkingNone(bool checked) +{ + if (! checked) + return; + QSettings settings; + settings.setValue("move_marking", "none"); + updateWindow(false); +} + +void MainWindow::setPlayToolTip() +{ + m_actionPlay->setToolTip( + m_computerColors[m_bd.get_to_play()] ? + tr("Make the computer continue to play the current color") : + tr("Make the computer play the current color")); +} + +void MainWindow::setRated(bool isRated) +{ + m_isRated = isRated; + if (isRated) + { + statusBar()->addWidget(m_ratedGameLabelText); + m_ratedGameLabelText->show(); + } + else if (m_ratedGameLabelText->isVisible()) + statusBar()->removeWidget(m_ratedGameLabelText); +} + +void MainWindow::setSetupPlayer() +{ + if (! m_game.has_setup()) + m_game.remove_player(); + else + m_game.set_player(m_bd.get_to_play()); +} + +void MainWindow::setTitleMenuLevel() +{ + QString title; + switch (m_game.get_variant()) + { + case Variant::classic: + title = tr("&Level (Classic, 4 Players)"); + break; + case Variant::classic_2: + title = tr("&Level (Classic, 2 Players)"); + break; + case Variant::classic_3: + title = tr("&Level (Classic, 3 Players)"); + break; + case Variant::duo: + title = tr("&Level (Duo)"); + break; + case Variant::trigon: + title = tr("&Level (Trigon, 4 Players)"); + break; + case Variant::trigon_2: + title = tr("&Level (Trigon, 2 Players)"); + break; + case Variant::trigon_3: + title = tr("&Level (Trigon, 3 Players)"); + break; + case Variant::junior: + title = tr("&Level (Junior)"); + break; + case Variant::nexos: + title = tr("&Level (Nexos, 4 Players)"); + break; + case Variant::nexos_2: + title = tr("&Level (Nexos, 2 Players)"); + break; + case Variant::callisto: + title = tr("&Level (Callisto, 4 Players)"); + break; + case Variant::callisto_2: + title = tr("&Level (Callisto, 2 Players)"); + break; + case Variant::callisto_3: + title = tr("&Level (Callisto, 3 Players)"); + break; + } + m_menuLevel->setTitle(title); +} + +void MainWindow::setupMode(bool enable) +{ + // Currently, we allow setup mode only if no moves have been played. It + // should also work in inner nodes but this might be confusing for users + // and violate some assumptions in the user interface (e.g. node depth is + // equal to move number). Therefore, m_actionSetupMode is disabled if the + // root node has children, but we still need to check for it here because + // due to bugs in the Unitiy interface in Ubuntu 11.10, menu items are + // not always disabled if the corresponding action is disabled. + if (enable && m_game.get_root().has_children()) + { + showInfo(tr("Setup mode cannot be used if moves have been played.")); + enable = false; + } + m_actionSetupMode->setChecked(enable); + m_guiBoard->setFreePlacement(enable); + if (enable) + { + m_setupModeLabel->show(); + for (Color c : m_bd.get_colors()) + m_pieceSelector[c]->setEnabled(true); + m_computerColors.fill(false); + } + else + { + setSetupPlayer(); + m_setupModeLabel->hide(); + enablePieceSelector(m_bd.get_to_play()); + updateWindow(false); + } +} + +void MainWindow::showComment(bool checked) +{ + QSettings settings; + bool wasVisible = m_comment->isVisible(); + if (wasVisible && ! checked) + settings.setValue("splitter_state", m_splitter->saveState()); + settings.setValue("show_comment", checked); + m_comment->setVisible(checked); + if (! wasVisible && checked) + m_splitter->restoreState( + settings.value("splitter_state").toByteArray()); + +} + +void MainWindow::showError(const QString& text, const QString& infoText, + const QString& detailText) +{ + ::showError(this, text, infoText, detailText); +} + +void MainWindow::showInfo(const QString& text, const QString& infoText, + const QString& detailText, bool withIcon) +{ + ::showInfo(this, text, infoText, detailText, withIcon); +} + +void MainWindow::showInvalidFile(QString file, const exception& e) +{ + showError(tr("Error in file '%1'").arg(QFileInfo(file).fileName()), + tr("The file is not a valid Blokus SGF file."), e.what()); +} + +void MainWindow::showRating() +{ + if (! m_ratingDialog) + { + m_ratingDialog = new RatingDialog(this, *m_history); + connect(m_ratingDialog, SIGNAL(openRecentFile(const QString&)), + SLOT(openCheckSave(const QString&))); + } + loadHistory(); + m_ratingDialog->show(); +} + +void MainWindow::showStatus(const QString& text, bool temporary) +{ + int timeout = (temporary ? 4000 : 0); + statusBar()->showMessage(text, timeout); +} + +void MainWindow::showToolbar(bool checked) +{ + QSettings settings; + settings.setValue("toolbar", checked); + findChild()->setVisible(checked); + m_menuToolBarText->setEnabled(checked); +} + +QSize MainWindow::sizeHint() const +{ + auto geo = QApplication::desktop()->screenGeometry(); + return QSize(geo.width() * 2 / 3, min(geo.width() * 4 / 10, geo.height())); +} + +void MainWindow::toolBarNoText(bool checked) +{ + if (checked) + toolBarText("no_text", Qt::ToolButtonIconOnly); +} + +void MainWindow::toolBarText(const QString& key, Qt::ToolButtonStyle style) +{ + QSettings settings; + settings.setValue("toolbar_text", key); + findChild()->setToolButtonStyle(style); + if (m_helpWindow) + m_helpWindow->findChild()->setToolButtonStyle(style); +} + +void MainWindow::toolBarTextBesideIcons(bool checked) +{ + if (checked) + toolBarText("beside_icons", Qt::ToolButtonTextBesideIcon); +} + +void MainWindow::toolBarTextBelowIcons(bool checked) +{ + if (checked) + toolBarText("below_icons", Qt::ToolButtonTextUnderIcon); +} + +void MainWindow::toolBarTextOnly(bool checked) +{ + if (checked) + toolBarText("text_only", Qt::ToolButtonTextOnly); +} + +void MainWindow::toolBarTextSystem(bool checked) +{ + if (checked) + toolBarText("system", Qt::ToolButtonFollowStyle); +} + +void MainWindow::truncate() +{ + auto& current = m_game.get_current(); + if (! current.has_parent()) + return; + cancelThread(); + if (current.has_children()) + { + QMessageBox msgBox(this); + initQuestion(msgBox, tr("Truncate this subtree?"), + tr("This position and all following moves and" + " variations will be removed from the game tree.")); + auto truncateButton = + msgBox.addButton(tr("Truncate"), + QMessageBox::DestructiveRole); + auto cancelButton = msgBox.addButton(QMessageBox::Cancel); + msgBox.setDefaultButton(cancelButton); + msgBox.exec(); + if (msgBox.clickedButton() != truncateButton) + return; + } + m_game.truncate(); + m_autoPlay = false; + m_gameFinished = false; + updateWindow(true); +} + +void MainWindow::truncateChildren() +{ + if (! m_game.get_current().has_children()) + return; + cancelThread(); + QMessageBox msgBox(this); + initQuestion(msgBox, tr("Truncate children?"), + tr("All following moves and variations will" + " be removed from the game tree.")); + auto truncateButton = + msgBox.addButton(tr("Truncate Children"), + QMessageBox::DestructiveRole); + auto cancelButton = msgBox.addButton(QMessageBox::Cancel); + msgBox.setDefaultButton(cancelButton); + msgBox.exec(); + if (msgBox.clickedButton() != truncateButton) + return; + m_game.truncate_children(); + m_gameFinished = false; + updateWindow(false); +} + +void MainWindow::showVariations(bool checked) +{ + { + QSettings settings; + settings.setValue("show_variations", checked); + } + updateWindow(false); +} + +void MainWindow::undo() +{ + auto& current = m_game.get_current(); + if (current.has_children() + || ! m_game.get_tree().has_move_ignore_invalid(current) + || ! current.has_parent()) + return; + cancelThread(); + m_game.undo(); + m_autoPlay = false; + m_gameFinished = false; + updateWindow(true); +} + +void MainWindow::updateComment() +{ + string comment = m_game.get_comment(); + if (comment.empty()) + { + setCommentText(""); + return; + } + string charset = m_game.get_root().get_property("CA", ""); + setCommentText(Util::convertSgfValueToQString(comment, charset)); +} + +void MainWindow::updateFlipActions() +{ + Piece piece = m_guiBoard->getSelectedPiece(); + if (piece.is_null()) + return; + auto transform = m_guiBoard->getSelectedPieceTransform(); + bool can_flip_horizontally = + m_bd.get_piece_info(piece).can_flip_horizontally(transform); + m_actionFlipHorizontally->setEnabled(can_flip_horizontally); + bool can_flip_vertically = + m_bd.get_piece_info(piece).can_flip_vertically(transform); + m_actionFlipVertically->setEnabled(can_flip_vertically); +} + +void MainWindow::updateMoveAnnotationActions() +{ + if (m_game.get_move_ignore_invalid().is_null()) + { + m_menuMoveAnnotation->setEnabled(false); + return; + } + m_menuMoveAnnotation->setEnabled(true); + double goodMove = m_game.get_good_move(); + if (goodMove > 1) + { + m_actionVeryGoodMove->setChecked(true); + return; + } + if (goodMove > 0) + { + m_actionGoodMove->setChecked(true); + return; + } + double badMove = m_game.get_bad_move(); + if (badMove > 1) + { + m_actionVeryBadMove->setChecked(true); + return; + } + if (badMove > 0) + { + m_actionBadMove->setChecked(true); + return; + } + if (m_game.is_interesting_move()) + { + m_actionInterestingMove->setChecked(true); + return; + } + if (m_game.is_doubtful_move()) + { + m_actionDoubtfulMove->setChecked(true); + return; + } + m_actionNoMoveAnnotation->setChecked(true); +} + +void MainWindow::updateMoveNumber() +{ + auto& tree = m_game.get_tree(); + auto& current = m_game.get_current(); + unsigned move = get_move_number(tree, current); + unsigned movesLeft = get_moves_left(tree, current); + unsigned totalMoves = move + movesLeft; + string variation = get_variation_string(current); + QString text = + QString::fromLocal8Bit(get_position_info(tree, current).c_str()); + QString toolTip; + if (variation.empty()) + { + if (movesLeft == 0) + { + if (move > 0) + toolTip = tr("Move %1").arg(move); + } + else + { + if (move == 0) + toolTip = tr("%n move(s)", "", totalMoves); + else + toolTip = tr("Move %1 of %2").arg(QString::number(move), + QString::number(totalMoves)); + } + } + else + toolTip = tr("Move %1 of %2 in variation %3") + .arg(QString::number(move), QString::number(totalMoves), + variation.c_str()); + if (text.isEmpty()) + { + if (m_moveNumber->isVisible()) + statusBar()->removeWidget(m_moveNumber); + } + else + { + m_moveNumber->setText(text); + m_moveNumber->setToolTip(toolTip); + if (! m_moveNumber->isVisible()) + { + statusBar()->addPermanentWidget(m_moveNumber); + m_moveNumber->show(); + } + } +} + +void MainWindow::updateRecentFiles() +{ + QSettings settings; + auto files = settings.value("recent_files").toStringList(); + for (int i = 0; i < files.size(); ++i) + if (! QFileInfo::exists(files[i])) + { + files.removeAt(i); + --i; + } + int nuRecentFiles = files.size(); + if (nuRecentFiles > maxRecentFiles) + nuRecentFiles = maxRecentFiles; + m_menuOpenRecent->setEnabled(nuRecentFiles > 0); + for (int i = 0; i < nuRecentFiles; ++i) + { + QFileInfo info = QFileInfo(files[i]); + QString name = info.absoluteFilePath(); + // Don't prepend the filename by a number for a shortcut key + // because the file name may contain underscores and Ubuntu Unity does + // not handle this correctly (Unity bug #1390373) + m_actionRecentFile[i]->setText(name); + m_actionRecentFile[i]->setData(files[i]); + m_actionRecentFile[i]->setVisible(true); + } + for (int j = nuRecentFiles; j < maxRecentFiles; ++j) + m_actionRecentFile[j]->setVisible(false); +} + +void MainWindow::updateWindow(bool currentNodeChanged) +{ + updateWindowModified(); + m_guiBoard->copyFromBoard(m_bd); + QSettings settings; + auto markVariations = settings.value("show_variations", true).toBool(); + unsigned nuMoves = m_bd.get_nu_moves(); + unsigned markMovesBegin, markMovesEnd; + if (m_actionMoveMarkingAllNumber->isChecked()) + { + markMovesBegin = 1; + markMovesEnd = nuMoves; + } + else if (m_actionMoveMarkingLastNumber->isChecked() + || m_actionMoveMarkingLastDot->isChecked()) + { + markMovesBegin = nuMoves; + markMovesEnd = nuMoves; + } + else + { + markMovesBegin = 0; + markMovesEnd = 0; + } + gui_board_util::setMarkup(*m_guiBoard, m_game, markMovesBegin, + markMovesEnd, markVariations, + m_actionMoveMarkingLastDot->isChecked()); + m_scoreDisplay->updateScore(m_bd); + if (m_legalMoves) + m_legalMoves->clear(); + m_legalMoveIndex = 0; + bool isGameOver = m_bd.is_game_over(); + auto to_play = m_bd.get_to_play(); + if (isGameOver && ! m_actionSetupMode->isChecked()) + m_orientationDisplay->clearSelectedColor(); + else + m_orientationDisplay->selectColor(to_play); + if (currentNodeChanged) + { + clearPiece(); + for (Color c : m_bd.get_colors()) + m_pieceSelector[c]->checkUpdate(); + if (! m_actionSetupMode->isChecked()) + enablePieceSelector(to_play); + updateComment(); + updateMoveAnnotationActions(); + } + updateMoveNumber(); + setPlayToolTip(); + auto& tree = m_game.get_tree(); + auto& current = m_game.get_current(); + bool isMain = is_main_variation(current); + bool hasEarlierVariation = has_earlier_variation(current); + bool hasParent = current.has_parent(); + bool hasChildren = current.has_children(); + bool hasMove = tree.has_move_ignore_invalid(current); + bool hasMoves = m_bd.has_moves(to_play); + bool isEmpty = libboardgame_sgf::util::is_empty(tree); + bool hasNextVar = current.get_sibling(); + bool hasPrevVar = current.get_previous_sibling(); + m_actionAnalyzeGame->setEnabled(! m_isRated + && tree.has_main_variation_moves()); + m_actionBackToMainVariation->setEnabled(! isMain); + m_actionBeginning->setEnabled(! m_isRated && hasParent); + m_actionBeginningOfBranch->setEnabled(hasEarlierVariation); + m_actionBackward->setEnabled(! m_isRated && hasParent); + m_actionComputerColors->setEnabled(! m_isRated); + m_actionDeleteAllVariations->setEnabled(tree.has_variations()); + m_actionFindNextComment->setEnabled(! m_isRated); + m_actionForward->setEnabled(hasChildren); + m_actionEnd->setEnabled(hasChildren); + m_actionFindMove->setEnabled(! isGameOver); + m_actionGotoMove->setEnabled(! m_isRated && + hasCurrentVariationOtherMoves(tree, current)); + m_actionKeepOnlyPosition->setEnabled(! m_isRated + && (hasParent || hasChildren)); + m_actionKeepOnlySubtree->setEnabled(hasParent && hasChildren); + m_actionGroupLevel->setEnabled(! m_isRated); + m_actionMakeMainVariation->setEnabled(! isMain); + m_actionMoveDownVariation->setEnabled(hasNextVar); + m_actionMoveUpVariation->setEnabled(hasPrevVar); + m_actionNew->setEnabled(! isEmpty); + m_actionNextVariation->setEnabled(hasNextVar); + if (! m_isGenMoveRunning) + { + m_actionNextPiece->setEnabled(! isGameOver); + m_actionPreviousPiece->setEnabled(! isGameOver); + m_actionPlay->setEnabled(! m_isRated && hasMoves); + m_actionPlaySingleMove->setEnabled(! m_isRated && hasMoves); + } + m_actionPreviousVariation->setEnabled(hasPrevVar); + m_actionRatedGame->setEnabled(! m_isRated); + m_actionSave->setEnabled(! m_file.isEmpty() && m_game.is_modified()); + m_actionSaveAs->setEnabled(! isEmpty || m_game.is_modified()); + // See also comment in setupMode() + m_actionSetupMode->setEnabled(! m_isRated && ! hasParent && ! hasChildren); + m_actionNextColor->setEnabled(! m_isRated); + m_actionTruncate->setEnabled(! m_isRated && hasParent); + m_actionTruncateChildren->setEnabled(hasChildren); + m_actionUndo->setEnabled(! m_isRated && hasParent && ! hasChildren + && hasMove); + m_actionGroupVariant->setEnabled(! m_isRated); + m_menuVariant->setEnabled(! m_isRated); + setTitleMenuLevel(); +} + +void MainWindow::updateWindowModified() +{ + if (! m_file.isEmpty()) + setWindowModified(m_game.is_modified()); +} + +void MainWindow::variantTriggered(bool checked) +{ + if (checked) + setVariant(Variant(qobject_cast(sender())->data().toInt())); +} + +void MainWindow::veryBadMove(bool checked) +{ + if (! checked) + return; + m_game.set_bad_move(2); + updateWindow(false); +} + +void MainWindow::veryGoodMove(bool checked) +{ + if (! checked) + return; + m_game.set_good_move(2); + updateWindow(false); +} + +void MainWindow::wheelEvent(QWheelEvent* event) +{ + int delta = event->delta() / 8 / 15; + if (delta > 0) + { + if (! m_guiBoard->getSelectedPiece().is_null()) + for (int i = 0; i < delta; ++i) + nextTransform(); + } + else if (delta < 0) + { + if (! m_guiBoard->getSelectedPiece().is_null()) + for (int i = 0; i < -delta; ++i) + previousTransform(); + } + event->accept(); +} + +bool MainWindow::writeGame(const string& file) +{ + ofstream out(file); + PentobiTreeWriter writer(out, m_game.get_tree()); + writer.set_indent(1); + writer.write(); + return static_cast(out); +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/MainWindow.h b/src/pentobi/MainWindow.h new file mode 100644 index 0000000..fd9332c --- /dev/null +++ b/src/pentobi/MainWindow.h @@ -0,0 +1,707 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/MainWindow.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_MAIN_WINDOW_H +#define PENTOBI_MAIN_WINDOW_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include "RatingHistory.h" +#include "libboardgame_util/RandomGenerator.h" +#include "libpentobi_base/ColorMap.h" +#include "libpentobi_base/Game.h" +#include "libpentobi_mcts/Player.h" + +class QActionGroup; +class QLabel; +class QPlainTextEdit; +class QSplitter; +class AnalyzeGameWindow; +class GuiBoard; +class HelpWindow; +class LeaveFullscreenButton; +class OrientationDisplay; +class PieceSelector; +class RatingDialog; +class ScoreDisplay; + +using namespace std; +using libboardgame_sgf::SgfNode; +using libboardgame_base::Transform; +using libboardgame_util::ArrayList; +using libboardgame_util::RandomGenerator; +using libpentobi_base::Board; +using libpentobi_base::ColorMap; +using libpentobi_base::ColorMove; +using libpentobi_base::Game; +using libpentobi_base::Move; +using libpentobi_base::MoveList; +using libpentobi_base::MoveMarker; +using libpentobi_base::Piece; +using libpentobi_base::Point; +using libpentobi_base::Variant; +using libpentobi_mcts::Player; + +//----------------------------------------------------------------------------- + +class MainWindow + : public QMainWindow +{ + Q_OBJECT + +public: + MainWindow(Variant variant, const QString& initialFile = "", + const QString& helpDir = "", + unsigned maxLevel = Player::max_supported_level, + const QString& booksDir = "", bool noBook = false, + unsigned nuThreads = 0); + + ~MainWindow(); + + bool eventFilter(QObject* object, QEvent* event) override; + + QSize sizeHint() const override; + +public slots: + void about(); + + void analyzeGame(); + + void backward(); + + void backToMainVariation(); + + void beginning(); + + void beginningOfBranch(); + + void clearPiece(); + + void computerColors(); + + void deleteAllVariations(); + + void end(); + + void exportAsciiArt(); + + void exportImage(); + + void findMove(); + + void findNextComment(); + + void flipHorizontally(); + + void flipVertically(); + + void forward(); + + void gotoMove(); + + /** Go to a node if a node with a position defined by a sequence of moves + still exists. */ + void gotoPosition(Variant variant, const vector& moves); + + void help(); + + void gameInfo(); + + /** Abort current move generation and don't play a move. */ + void interrupt(); + + /** Abort current move generation and play best move found so far. */ + void interruptPlay(); + + void keepOnlyPosition(); + + void keepOnlySubtree(); + + void makeMainVariation(); + + void moveDownVariation(); + + void moveUpVariation(); + + void newGame(); + + void nextColor(); + + void nextVariation(); + + void nextPiece(); + + void nextTransform(); + + void open(); + + bool open(const QString& file, bool isTemporary = false); + + void placePiece(Color c, Move mv); + + void play(); + + void playSingleMove(); + + void pointClicked(Point p); + + void previousPiece(); + + void previousTransform(); + + void previousVariation(); + + void ratedGame(); + + void rotateAnticlockwise(); + + void rotateClockwise(); + + void save(); + + void saveAs(); + + void selectPiece(Color c, Piece piece); + + void selectPiece(Color c, Piece piece, const Transform* transform); + + void setLevel(unsigned level); + + void truncate(); + + void truncateChildren(); + + void undo(); + + void showToolbar(bool checked); + + void showVariations(bool checked); + + void showRating(); + + void setNoDelay(); + +protected: + void closeEvent(QCloseEvent* event) override; + + void wheelEvent(QWheelEvent* event) override; + +private: + struct GenMoveResult + { + bool playSingleMove; + + Color color; + + Move move; + + unsigned genMoveId; + }; + + static const int maxRecentFiles = 9; + + Game m_game; + + const Board& m_bd; + + unique_ptr m_player; + + bool m_noDelay = false; + + /** Was window maximized before entering fullscreen. */ + bool m_wasMaximized = false; + + bool m_isGenMoveRunning = false; + + bool m_isAnalyzeRunning = false; + + /** Should the computer generate a move if it is its turn? + Enabled on game start (if the computer plays at least one color) + or after selecting Play. Disabled when navigating in the game. */ + bool m_autoPlay = false; + + /** Flag indicating that the position after the last move played was + a terminal position. */ + bool m_gameFinished; + + bool m_isRated = false; + + /** Flag set while setting the text in m_comment for fast return in the + textChanged() handler. + Used because QPlainTextEdit does not have a textEdited() signal and + we only need to handle edits. */ + bool m_ignoreCommentTextChanged = false; + + /** Color played by the user in a rated game. + Only defined if m_isRated is true. In game variants with multiple + colors per player, the user plays all colors of the player with + this color. */ + Color m_ratedGameColor; + + /** Integer ID assigned to the currently running move generation. + Used to ignore finished events from canceled move generations. */ + unsigned m_genMoveId = 0; + + unsigned m_maxLevel; + + /** Current playing level of m_player. + Only use if m_useTimeLimit is false. Possible values for m_level are in + 1..maxLevel. Only used if m_timeLimit is zero. Stored independently of + the player and set at the player before each move generation, such that + setting a new level does not require to abort a running move + generation. */ + unsigned m_level; + + RandomGenerator m_random; + + unique_ptr m_history; + + /** Local variable in findMove(). + Reused for efficiency. */ + unique_ptr m_marker; + + GuiBoard* m_guiBoard; + + QString m_helpDir; + + ColorMap m_computerColors; + + ColorMap m_pieceSelector; + + OrientationDisplay* m_orientationDisplay; + + ScoreDisplay* m_scoreDisplay; + + QSplitter* m_splitter; + + QPlainTextEdit* m_comment; + + HelpWindow* m_helpWindow = nullptr; + + RatingDialog* m_ratingDialog = nullptr; + + AnalyzeGameWindow* m_analyzeGameWindow = nullptr; + + QAction* m_actionAbout; + + QAction* m_actionAnalyzeGame; + + QAction* m_actionBackward; + + QAction* m_actionBackToMainVariation; + + QAction* m_actionBadMove; + + QAction* m_actionBeginning; + + QAction* m_actionBeginningOfBranch; + + QAction* m_actionClearPiece; + + QAction* m_actionComputerColors; + + QAction* m_actionCoordinates; + + QAction* m_actionDeleteAllVariations; + + QAction* m_actionDoubtfulMove; + + QAction* m_actionEnd; + + QAction* m_actionExportAsciiArt; + + QAction* m_actionExportImage; + + QAction* m_actionFindMove; + + QAction* m_actionFindNextComment; + + QAction* m_actionFlipHorizontally; + + QAction* m_actionFlipVertically; + + QAction* m_actionForward; + + QAction* m_actionFullscreen; + + QAction* m_actionGameInfo; + + QAction* m_actionGoodMove; + + QAction* m_actionGotoMove; + + QAction* m_actionHelp; + + QAction* m_actionInterestingMove; + + QAction* m_actionInterrupt; + + QAction* m_actionInterruptPlay; + + QAction* m_actionKeepOnlyPosition; + + QAction* m_actionKeepOnlySubtree; + + QAction* m_actionLeaveFullscreen; + + QAction* m_actionMakeMainVariation; + + QAction* m_actionMoveDownVariation; + + QAction* m_actionMoveMarkingAllNumber; + + QAction* m_actionMoveMarkingLastDot; + + QAction* m_actionMoveMarkingLastNumber; + + QAction* m_actionMoveMarkingNone; + + QAction* m_actionMoveUpVariation; + + QAction* m_actionMovePieceLeft; + + QAction* m_actionMovePieceRight; + + QAction* m_actionMovePieceUp; + + QAction* m_actionMovePieceDown; + + QAction* m_actionNextColor; + + QAction* m_actionNextPiece; + + QAction* m_actionNextTransform; + + QAction* m_actionNextVariation; + + QAction* m_actionNew; + + QAction* m_actionRatedGame; + + QAction* m_actionNoMoveAnnotation; + + QAction* m_actionOpen; + + QAction* m_actionPlacePiece; + + QAction* m_actionPlay; + + QAction* m_actionPlaySingleMove; + + QAction* m_actionPreviousPiece; + + QAction* m_actionPreviousTransform; + + QAction* m_actionPreviousVariation; + + QAction* m_actionQuit; + + QAction* m_actionRecentFile[maxRecentFiles]; + + QAction* m_actionRotateAnticlockwise; + + QAction* m_actionRotateClockwise; + + QAction* m_actionSave; + + QAction* m_actionSaveAs; + + QAction* m_actionShowComment; + + QAction* m_actionRating; + + QAction* m_actionShowToolbar; + + QAction* m_actionSetupMode; + + QAction* m_actionToolBarNoText; + + QAction* m_actionToolBarTextBesideIcons; + + QAction* m_actionToolBarTextBelowIcons; + + QAction* m_actionToolBarTextOnly; + + QAction* m_actionToolBarTextSystem; + + QAction* m_actionTruncate; + + QAction* m_actionTruncateChildren; + + QAction* m_actionShowVariations; + + QAction* m_actionUndo; + + QAction* m_actionVariantCallisto; + + QAction* m_actionVariantCallisto2; + + QAction* m_actionVariantCallisto3; + + QAction* m_actionVariantClassic; + + QAction* m_actionVariantClassic2; + + QAction* m_actionVariantClassic3; + + QAction* m_actionVariantDuo; + + QAction* m_actionVariantJunior; + + QAction* m_actionVariantNexos; + + QAction* m_actionVariantNexos2; + + QAction* m_actionVariantTrigon; + + QAction* m_actionVariantTrigon2; + + QAction* m_actionVariantTrigon3; + + QAction* m_actionVeryGoodMove; + + QAction* m_actionVeryBadMove; + + QActionGroup* m_actionGroupLevel; + + QActionGroup* m_actionGroupVariant; + + QMenu* m_menuExport; + + QMenu* m_menuLevel; + + QMenu* m_menuMoveAnnotation; + + QMenu* m_menuOpenRecent; + + QMenu* m_menuToolBarText; + + QMenu* m_menuVariant; + + QLabel* m_setupModeLabel; + + QLabel* m_ratedGameLabelText; + + QFutureWatcher m_genMoveWatcher; + + QString m_file; + + unique_ptr m_legalMoves; + + unsigned m_legalMoveIndex; + + QLabel* m_moveNumber; + + LeaveFullscreenButton* m_leaveFullscreenButton = nullptr; + + int m_lastRemainingSeconds; + + int m_lastRemainingMinutes; + + /** Is the current game a game loaded from the autosave file? + If yes, we need it to save again on quit even if it was not modified. + Note that the autosave game is deleted after loading to avoid that + it is used twice if two instances of Pentobi are started. */ + bool m_isAutoSaveLoaded; + + + GenMoveResult asyncGenMove(Color c, int genMoveId, bool playSingleMove); + + bool checkSave(); + + bool checkQuit(); + + void clearFile(); + + QAction* createAction(const QString& text = ""); + + QAction* createActionLevel(unsigned level, const QString& text); + + void createActions(); + + QAction* createActionVariant(Variant variant, const QString& text); + + QWidget* createCentralWidget(); + + QWidget* createLeftPanel(); + + void createMenu(); + + QLayout* createOrientationButtonBoxLeft(); + + QLayout* createOrientationButtonBoxRight(); + + QLayout* createOrientationSelector(); + + QLayout* createRightPanel(); + + void createToolBar(); + + void cancelThread(); + + void checkComputerMove(); + + void clearStatus(); + + bool computerPlaysAll() const; + + void deleteAutoSaveFile(); + + void enablePieceSelector(Color c); + + void gameOver(); + + void genMove(bool playSingleMove = false); + + QString getFilter() const; + + QString getLastDir(); + + QString getVersion() const; + + void gotoNode(const SgfNode& node); + + void gotoNode(const SgfNode* node); + + void initGame(); + + void initVariantActions(); + + void initPieceSelectors(); + + bool isComputerToPlay() const; + + void leaveSetupMode(); + + void play(Color c, Move mv); + + void restoreLevel(Variant variant); + + bool save(const QString& file); + + void searchCallback(double elapsedSeconds, double remainingSeconds); + + void setCommentText(const QString& text); + + void setVariant(Variant variant); + + void setPlayToolTip(); + + void setRated(bool isRated); + + void setFile(const QString& file); + + void showError(const QString& message, const QString& infoText = "", + const QString& detailText = ""); + + void showInfo(const QString& message, const QString& infoText = "", + const QString& detailText = "", bool withIcon = false); + + void showInvalidFile(QString file, const exception& e); + + void showStatus(const QString& text, bool temporary = false); + + void updateMoveNumber(); + + void updateWindow(bool currentNodeChanged); + + void updateWindowModified(); + + void updateComment(); + + void updateMoveAnnotationActions(); + + void loadHistory(); + + void updateRecentFiles(); + + void updateFlipActions(); + + bool writeGame(const string& file); + +private slots: + void analyzeGameFinished(); + + void badMove(bool checked); + + void commentChanged(); + + void continueRatedGame(); + + void coordinates(bool checked); + + void doubtfulMove(bool checked); + + void fullscreen(); + + void genMoveFinished(); + + void goodMove(bool checked); + + void interestingMove(bool checked); + + void leaveFullscreen(); + + void levelTriggered(bool checked); + + void noMoveAnnotation(bool checked); + + void openCheckSave(const QString& file); + + void openRecentFile(); + + void orientationDisplayColorClicked(Color c); + + void rememberFile(const QString& file); + + void rememberDir(const QString& file); + + void selectNamedPiece(); + + void setMoveMarkingAllNumber(bool checked); + + void setMoveMarkingLastNumber(bool checked); + + void setMoveMarkingLastDot(bool checked); + + void setMoveMarkingNone(bool checked); + + void setSetupPlayer(); + + void setTitleMenuLevel(); + + void setupMode(bool checked); + + void showComment(bool checked); + + void toolBarNoText(bool checked); + + void toolBarText(const QString& key, Qt::ToolButtonStyle style); + + void toolBarTextBesideIcons(bool checked); + + void toolBarTextBelowIcons(bool checked); + + void toolBarTextOnly(bool checked); + + void toolBarTextSystem(bool checked); + + void veryBadMove(bool checked); + + void veryGoodMove(bool checked); + + void variantTriggered(bool checked); +}; + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_MAIN_WINDOW_H diff --git a/src/pentobi/RatedGamesList.cpp b/src/pentobi/RatedGamesList.cpp new file mode 100644 index 0000000..ef4a51b --- /dev/null +++ b/src/pentobi/RatedGamesList.cpp @@ -0,0 +1,127 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/RatedGamesList.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "RatedGamesList.h" + +#include +#include +#include +#include "libboardgame_util/Log.h" +#include "libpentobi_gui/Util.h" + +//----------------------------------------------------------------------------- + +RatedGamesList::RatedGamesList(QWidget* parent) + : QTableView(parent) +{ + verticalHeader()->setVisible(false); + setShowGrid(false); + setEditTriggers(QAbstractItemView::NoEditTriggers); + setTabKeyNavigation(false); + setSelectionBehavior(QAbstractItemView::SelectRows); + setAlternatingRowColors(true); + m_model = new QStandardItemModel(this); + setModel(m_model); + connect(this, SIGNAL(doubleClicked(const QModelIndex&)), + SLOT(activateGame(const QModelIndex&))); +} + +void RatedGamesList::activateGame(const QModelIndex& index) +{ + auto item = m_model->item(index.row(), 0); + if (! item) + return; + bool ok; + unsigned n = item->text().toUInt(&ok); + if (ok) + emit openRatedGame(n); +} + +void RatedGamesList::focusInEvent(QFocusEvent* event) +{ + // Select current index if list has focus + selectRow(currentIndex().row()); + scrollTo(currentIndex()); + QTableView::focusInEvent(event); +} + +void RatedGamesList::focusOutEvent(QFocusEvent* event) +{ + // Show selection only if list has focus + clearSelection(); + QTableView::focusOutEvent(event); +} + +void RatedGamesList::keyPressEvent(QKeyEvent* event) +{ + if (event->type() == QEvent::KeyPress + && static_cast(event)->key() == Qt::Key_Space) + { + QModelIndexList indexes = + selectionModel()->selection().indexes(); + if (! indexes.isEmpty()) + activateGame(indexes[0]); + return; + } + QTableView::keyPressEvent(event); +} + +void RatedGamesList::updateContent(Variant variant, + const RatingHistory& history) +{ + m_model->clear(); + QStringList headers; + headers << tr("Game") << tr("Your Color") << tr("Level") << tr("Result") + << tr("Date"); + m_model->setHorizontalHeaderLabels(headers); + auto header = horizontalHeader(); + header->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter); + header->setHighlightSections(false); + header->setSectionResizeMode(QHeaderView::ResizeToContents); + header->setStretchLastSection(true); + int nuRows = 0; + if (history.getGameInfos().size() + <= static_cast(numeric_limits::max())) + nuRows = static_cast(history.getGameInfos().size()); + m_model->setRowCount(nuRows); + setSortingEnabled(false); + for (int i = 0; i < nuRows; ++i) + { + auto& info = history.getGameInfos()[i]; + auto number = new QStandardItem; + number->setData(info.number, Qt::DisplayRole); + auto color = new QStandardItem; + if (info.color.to_int() < get_nu_colors(variant)) + color->setText(Util::getPlayerString(variant, info.color)); + else + LIBBOARDGAME_LOG("Error: invalid color in rating history"); + auto level = new QStandardItem; + level->setData(info.level, Qt::DisplayRole); + QString result; + if (info.result == 1) + result = tr("Win"); + else if (info.result == 0.5) + result = tr("Tie"); + else if (info.result == 0) + result = tr("Loss"); + int row = nuRows - i - 1; + m_model->setItem(row, 0, number); + m_model->setItem(row, 1, color); + m_model->setItem(row, 2, level); + m_model->setItem(row, 3, new QStandardItem(result)); + m_model->setItem(row, 4, new QStandardItem(info.date)); + } + setSortingEnabled(true); + if (nuRows > 0) + selectionModel()->setCurrentIndex(model()->index(0, 0), + QItemSelectionModel::NoUpdate); +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/RatedGamesList.h b/src/pentobi/RatedGamesList.h new file mode 100644 index 0000000..8426850 --- /dev/null +++ b/src/pentobi/RatedGamesList.h @@ -0,0 +1,51 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/RatedGamesList.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_RATED_GAMES_LIST +#define PENTOBI_RATED_GAMES_LIST + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include "RatingHistory.h" + +class QStandardItemModel; + +//----------------------------------------------------------------------------- + +class RatedGamesList + : public QTableView +{ + Q_OBJECT + +public: + explicit RatedGamesList(QWidget* parent = nullptr); + + void updateContent(Variant variant, const RatingHistory& history); + +signals: + void openRatedGame(unsigned n); + +protected: + void focusInEvent(QFocusEvent* event) override; + + void focusOutEvent(QFocusEvent* event) override; + + void keyPressEvent(QKeyEvent* event) override; + +private: + QStandardItemModel* m_model; + +private slots: + void activateGame(const QModelIndex& index); +}; + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_RATED_GAMES_LIST diff --git a/src/pentobi/RatingDialog.cpp b/src/pentobi/RatingDialog.cpp new file mode 100644 index 0000000..40edf5b --- /dev/null +++ b/src/pentobi/RatingDialog.cpp @@ -0,0 +1,172 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/RatingDialog.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "RatingDialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "Util.h" + +//----------------------------------------------------------------------------- + +QLabel* createSelectableLabel() +{ + auto label = new QLabel; + label->setTextInteractionFlags(Qt::TextSelectableByMouse); + return label; +} + +//----------------------------------------------------------------------------- + +RatingDialog::RatingDialog(QWidget* parent, RatingHistory& history) + : QDialog(parent), + m_history(history) +{ + setWindowTitle(tr("Rating")); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + auto layout = new QVBoxLayout; + setLayout(layout); + auto formLayout = new QFormLayout; + layout->addLayout(formLayout); + formLayout->setLabelAlignment(Qt::AlignLeft); + auto box = new QHBoxLayout; + m_labelRating = createSelectableLabel(); + box->addWidget(m_labelRating); + box->addStretch(); + formLayout->addRow(tr("Your rating:"), box); + m_labelVariant = createSelectableLabel(); + formLayout->addRow(tr("Game variant:"), m_labelVariant); + m_labelNuGames = createSelectableLabel(); + formLayout->addRow(tr("Number rated games:"), m_labelNuGames); + m_labelBestRating = createSelectableLabel(); + formLayout->addRow(tr("Best previous rating:"), m_labelBestRating); + layout->addSpacing(layout->margin()); + layout->addWidget(new QLabel(tr("Recent development:"))); + m_graph = new RatingGraph; + layout->addWidget(m_graph, 1); + layout->addSpacing(layout->margin()); + layout->addWidget(new QLabel(tr("Recent games:"))); + m_list = new RatedGamesList; + layout->addWidget(m_list, 1); + auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Close); + layout->addWidget(buttonBox); + m_clearButton = + buttonBox->addButton(tr("&Clear"), QDialogButtonBox::ActionRole); + buttonBox->button(QDialogButtonBox::Close)->setDefault(true); + buttonBox->button(QDialogButtonBox::Close)->setAutoDefault(true); + buttonBox->button(QDialogButtonBox::Close)->setFocus(); + updateContent(); + connect(buttonBox, SIGNAL(rejected()), SLOT(reject())); + connect(buttonBox, SIGNAL(clicked(QAbstractButton*)), + SLOT(buttonClicked(QAbstractButton*))); + connect(m_list, SIGNAL(openRatedGame(unsigned)), + SLOT(activateGame(unsigned))); +} + +void RatingDialog::activateGame(unsigned n) +{ + emit openRecentFile(m_history.getFile(n)); +} + +void RatingDialog::buttonClicked(QAbstractButton* button) +{ + if (button != static_cast(m_clearButton)) + return; + QMessageBox msgBox(QMessageBox::Warning, "", + tr("Clear rating and delete rating history?"), + QMessageBox::Cancel, this); + Util::setNoTitle(msgBox); + auto clearButton = + msgBox.addButton(tr("Clear rating"), QMessageBox::DestructiveRole); + msgBox.setDefaultButton(clearButton); + msgBox.exec(); + if (msgBox.clickedButton() != clearButton) + return; + m_history.clear(); + updateContent(); +} + +void RatingDialog::updateContent() +{ + auto variant = m_history.getVariant(); + unsigned nuGames = m_history.getNuGames(); + Rating rating = m_history.getRating(); + Rating bestRating = m_history.getBestRating(); + if (nuGames == 0) + rating = Rating(0); + QString variantStr; + switch (variant) + { + case Variant::classic: + variantStr = tr("Classic (4 players)"); + break; + case Variant::classic_2: + variantStr = tr("Classic (2 players)"); + break; + case Variant::classic_3: + variantStr = tr("Classic (3 players)"); + break; + case Variant::duo: + variantStr = tr("Duo"); + break; + case Variant::trigon: + variantStr = tr("Trigon (4 players)"); + break; + case Variant::trigon_2: + variantStr = tr("Trigon (2 players)"); + break; + case Variant::trigon_3: + variantStr = tr("Trigon (3 players)"); + break; + case Variant::junior: + variantStr = tr("Junior"); + break; + case Variant::nexos: + variantStr = tr("Nexos (4 players)"); + break; + case Variant::nexos_2: + variantStr = tr("Nexos (2 players)"); + break; + case Variant::callisto: + variantStr = tr("Callisto (4 players)"); + break; + case Variant::callisto_2: + variantStr = tr("Callisto (2 players)"); + break; + case Variant::callisto_3: + variantStr = tr("Callisto (3 players)"); + break; + } + m_labelVariant->setText(variantStr); + m_labelNuGames->setText(QString::number(nuGames)); + if (nuGames == 0) + { + m_labelRating->setText("--"); + m_labelBestRating->setText("--"); + } + else + { + m_labelRating->setText(QString("%1").arg(rating.to_int())); + m_labelBestRating->setNum(bestRating.to_int()); + } + m_graph->updateContent(m_history); + m_list->updateContent(variant, m_history); + m_clearButton->setEnabled(nuGames > 0); +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/RatingDialog.h b/src/pentobi/RatingDialog.h new file mode 100644 index 0000000..195409c --- /dev/null +++ b/src/pentobi/RatingDialog.h @@ -0,0 +1,69 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/RatingDialog.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_RATING_DIALOG_H +#define PENTOBI_RATING_DIALOG_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include "RatedGamesList.h" +#include "RatingGraph.h" +#include "libpentobi_base/Variant.h" + +class QAbstractButton; +class QLabel; + +using namespace std; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +class RatingDialog + : public QDialog +{ + Q_OBJECT + +public: + /** Constructor. + @param parent + @param history (@ref libboardgame_doc_storesref) */ + RatingDialog(QWidget* parent, RatingHistory& history); + + void updateContent(); + +signals: + void openRecentFile(const QString& file); + +private: + RatingHistory& m_history; + + QPushButton* m_clearButton; + + QLabel* m_labelVariant; + + QLabel* m_labelNuGames; + + QLabel* m_labelRating; + + QLabel* m_labelBestRating; + + RatingGraph* m_graph; + + RatedGamesList* m_list; + +private slots: + void activateGame(unsigned n); + + void buttonClicked(QAbstractButton*); +}; + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_RATING_DIALOG_H diff --git a/src/pentobi/RatingGraph.cpp b/src/pentobi/RatingGraph.cpp new file mode 100644 index 0000000..c01d4f7 --- /dev/null +++ b/src/pentobi/RatingGraph.cpp @@ -0,0 +1,121 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/RatingGraph.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "RatingGraph.h" + +#include +#include +#include +#include + +//----------------------------------------------------------------------------- + +RatingGraph::RatingGraph(QWidget* parent) + : QFrame(parent) +{ + setMinimumSize(200, 60); + setFrameStyle(QFrame::StyledPanel | QFrame::Sunken); +} + +void RatingGraph::paintEvent(QPaintEvent* event) +{ + QFrame::paintEvent(event); + QRect contentsRect = QFrame::contentsRect(); + int width = contentsRect.width(); + int height = contentsRect.height(); + QPainter painter(this); + painter.translate(contentsRect.x(), contentsRect.y()); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setPen(Qt::NoPen); + painter.setBrush(QColor(255, 255, 255)); + painter.drawRect(0, 0, width, height); + if (! m_values.empty()) + { + QFontMetrics metrics(painter.font()); + float yRange = m_yMax - m_yMin; + float yTic = m_yMin; + float topMargin = ceil(1.2f * static_cast(metrics.height())); + float bottomMargin = ceil(0.3f * static_cast(metrics.height())); + float graphHeight = + static_cast(height) - topMargin - bottomMargin; + QPen pen(QColor(96, 96, 96)); + pen.setStyle(Qt::DotLine); + painter.setPen(pen); + int maxLabelWidth = 0; + while (yTic <= m_yMax) + { + int y = + static_cast(round( + topMargin + + graphHeight - (yTic - m_yMin) / yRange * graphHeight)); + painter.drawLine(0, y, width, y); + QString label; + label.setNum(yTic, 'f', 0); + int labelWidth = metrics.width(label + " "); + maxLabelWidth = max(maxLabelWidth, labelWidth); + painter.drawText(width - labelWidth, y - metrics.descent(), + label); + if (yRange < 600) + yTic += 100; + else + yTic += 200; + } + qreal dX = qreal(width - maxLabelWidth) / RatingHistory::maxGames; + qreal x = 0; + QPainterPath path; + for (unsigned i = 0; i < m_values.size(); ++i) + { + qreal y = + topMargin + + graphHeight - (m_values[i] - m_yMin) / yRange * graphHeight; + if (i == 0) + path.moveTo(x, y); + else + path.lineTo(x, y); + x += dX; + } + painter.setPen(Qt::red); + painter.setBrush(Qt::NoBrush); + painter.drawPath(path); + } +} + +QSize RatingGraph::sizeHint() const +{ + auto geo = QApplication::desktop()->screenGeometry(); + return QSize(geo.width() / 3, min(geo.width() / 12, geo.height() / 3)); +} + +void RatingGraph::updateContent(const RatingHistory& history) +{ + m_values.clear(); + auto& games = history.getGameInfos(); + if (games.empty()) + { + update(); + return; + } + m_yMin = games[0].rating.get(); + m_yMax = m_yMin; + for (const RatingHistory::GameInfo& info : games) + { + float rating = info.rating.get(); + m_yMin = min(m_yMin, rating); + m_yMax = max(m_yMax, rating); + m_values.push_back(rating); + } + m_yMin = floor((m_yMin / 100.f)) * 100; + m_yMax = ceil((m_yMax / 100.f)) * 100; + if (m_yMax == m_yMin) + m_yMax = m_yMin + 100; + update(); +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/RatingGraph.h b/src/pentobi/RatingGraph.h new file mode 100644 index 0000000..b737c9a --- /dev/null +++ b/src/pentobi/RatingGraph.h @@ -0,0 +1,45 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/RatingGraph.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_RATING_GRAPH_H +#define PENTOBI_RATING_GRAPH_H + +// Needed in the header because moc_*.cxx does not include config.h +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include "RatingHistory.h" + +//----------------------------------------------------------------------------- + +class RatingGraph + : public QFrame +{ + Q_OBJECT + +public: + explicit RatingGraph(QWidget* parent = nullptr); + + void updateContent(const RatingHistory& history); + + QSize sizeHint() const override; + +protected: + void paintEvent(QPaintEvent* event) override; + +private: + float m_yMin; + + float m_yMax; + + vector m_values; +}; + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_RATING_GRAPH_H diff --git a/src/pentobi/RatingHistory.cpp b/src/pentobi/RatingHistory.cpp new file mode 100644 index 0000000..5a27f62 --- /dev/null +++ b/src/pentobi/RatingHistory.cpp @@ -0,0 +1,194 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/RatingHistory.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "RatingHistory.h" + +#include +#include +#include +#include +#include +#include +#include "Util.h" +#include "libpentobi_base/PentobiTreeWriter.h" +#include "libpentobi_mcts/Player.h" + +using libpentobi_base::to_string_id; +using libpentobi_base::PentobiTreeWriter; +using libpentobi_mcts::Player; + +//----------------------------------------------------------------------------- + +namespace { + +/** 1000 Elo represents a beginner level. */ +const float startRating = 1000; + +QString getRatedGamesDir(Variant variant) +{ + return + Util::getDataDir() + "/rated_games/" + QString(to_string_id(variant)); +} + +} // namespace + +//----------------------------------------------------------------------------- + +RatingHistory::RatingHistory(Variant variant) +{ + load(variant); +} + +void RatingHistory::addGame(float score, Rating opponentRating, + unsigned nuOpponents, Color color, + float result, const QString& date, int level, + const PentobiTree& tree) +{ + float kValue = (m_nuGames < 30 ? 40.f : 20.f); + m_rating.update(score, opponentRating, kValue, nuOpponents); + if (m_rating.get() > m_bestRating.get()) + m_bestRating = m_rating; + ++m_nuGames; + GameInfo info; + info.number = m_nuGames; + info.color = color; + info.result = result; + info.date = date; + info.level = level; + info.rating = m_rating; + m_games.push_back(info); + size_t nuGames = m_games.size(); + if (nuGames > maxGames) + m_games.erase(m_games.begin(), m_games.begin() + nuGames - maxGames); + save(); + ofstream out(getFile(m_nuGames).toLocal8Bit().constData()); + PentobiTreeWriter writer(out, tree); + writer.set_indent(1); + writer.write(); + // Only save the last RatingHistory::maxGames games + if (m_nuGames > maxGames) + QFile::remove(getFile(m_nuGames - maxGames)); +} + +void RatingHistory::clear() +{ + QString variantStr = QString(to_string_id(m_variant)); + QSettings settings; + settings.remove("rated_games_" + variantStr); + settings.remove("rating_" + variantStr); + settings.remove("best_rating_" + variantStr); + for (const RatingHistory::GameInfo& info : getGameInfos()) + QFile::remove(getFile(info.number)); + QFile::remove(m_file); + m_nuGames = 0; + m_rating = Rating(startRating); + m_bestRating = Rating(startRating); + m_games.clear(); +} + +QString RatingHistory::getFile(unsigned n) const +{ + return QString("%1/%2.blksgf").arg(m_dir, QString::number(n)); +} + +void RatingHistory::getNextRatedGameSettings(int maxLevel, unsigned random, + int& level, Color& color) +{ + color = + Color(static_cast(random % get_nu_players(m_variant))); + float minDiff = 0; // Initialize to avoid compiler warning + for (int i = 1; i <= maxLevel; ++i) + { + float diff = + abs(m_rating.get() - Player::get_rating(m_variant, i).get()); + if (i == 1 || diff < minDiff) + { + minDiff = diff; + level = i; + } + } +} + +void RatingHistory::init(Rating rating) +{ + m_rating = rating; + m_bestRating = rating; + m_nuGames = 0; + m_games.clear(); + save(); +} + +void RatingHistory::load(Variant variant) +{ + m_variant = variant; + QString variantStr = QString(to_string_id(variant)); + QSettings settings; + m_nuGames = settings.value("rated_games_" + variantStr, 0).toUInt(); + // Default value is 1000 (Elo-rating for beginner-level play) + m_rating = + Rating(settings.value("rating_" + variantStr, startRating).toFloat()); + m_bestRating = + Rating(settings.value("best_rating_" + variantStr, 0).toFloat()); + m_games.clear(); + m_dir = getRatedGamesDir(variant); + m_file = m_dir + "/history.dat"; + ifstream file(m_file.toLocal8Bit().constData()); + if (! file) + return; + string line; + while (getline(file, line) && m_games.size() < maxGames) + { + istringstream in(line); + GameInfo info; + unsigned c; + string date; + in >> info.number >> c >> info.result >> date >> info.level + >> info.rating; + info.date = QString(date.c_str()); + if (! in || c >= get_nu_colors(variant)) + return; + info.color = Color(static_cast(c)); + if (info.number >= 1 && info.number <= m_nuGames) + m_games.push_back(info); + } + size_t nuGames = m_games.size(); + if (nuGames > maxGames) + m_games.erase(m_games.begin(), m_games.begin() + nuGames - maxGames); + // Make the all-time best rating consistent with the rating history. Older + // versions of Pentobi (up to version 3) did not save the all-time best + // rating, so after an upgrade to a newer version of Pentobi, the history + // of recent rated games can contain a higher rating than the stored + // all-time best rating. + for (const RatingHistory::GameInfo& info : getGameInfos()) + if (info.rating.get() > m_bestRating.get()) + m_bestRating = info.rating; +} + +void RatingHistory::save() const +{ + QString variantStr = QString(to_string_id(m_variant)); + QSettings settings; + settings.setValue("rated_games_" + variantStr, m_nuGames); + settings.setValue("rating_" + variantStr, + static_cast(m_rating.get())); + settings.setValue("best_rating_" + variantStr, + static_cast(m_bestRating.get())); + LIBBOARDGAME_ASSERT(! m_file.isEmpty()); + QDir dir(""); + dir.mkpath(m_dir); + ofstream out(m_file.toLocal8Bit().constData()); + for (auto& info : m_games) + out << info.number << ' ' << static_cast(info.color.to_int()) + << ' ' << info.result << ' ' + << info.date.toLocal8Bit().constData() << ' ' << info.level + << ' ' << info.rating << '\n'; +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/RatingHistory.h b/src/pentobi/RatingHistory.h new file mode 100644 index 0000000..568fe5a --- /dev/null +++ b/src/pentobi/RatingHistory.h @@ -0,0 +1,140 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/RatingHistory.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_RATING_HISTORY_H +#define PENTOBI_RATING_HISTORY_H + +#include +#include +#include "libboardgame_base/Rating.h" +#include "libpentobi_base/Color.h" +#include "libpentobi_base/PentobiTree.h" +#include "libpentobi_base/Variant.h" + +using namespace std; +using libboardgame_base::Rating; +using libpentobi_base::Color; +using libpentobi_base::PentobiTree; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +/** History of rated games in a certain game variant. */ +class RatingHistory +{ +public: + /** Maximum number of games to remember in the history. */ + static const unsigned maxGames = 100; + + struct GameInfo + { + /** Game number. + The first game played has number 0. */ + unsigned number; + + /** Color played by the human. + In game variants with multiple colors per player, the human played + all colors played by the player of this color. */ + Color color; + + /** Game result. + 0=Loss, 0.5=tie, 1=win from the viewpoint of the human. */ + float result; + + /** Date of the game in "YYYY-MM-DD" format. */ + QString date; + + /** The playing level of the computer opponent. */ + int level; + + /** The rating of the human after the game. */ + Rating rating; + }; + + + explicit RatingHistory(Variant variant); + + /** Initialize rating to a given a-priori value. */ + void init(Rating rating); + + /** Get level and user color for next rated games. + @param maxLevel The maximum playing level. + @param random A random number to determine the color for the human. + @param[out] level The playing level for the next game. + @param[out] color The color for the human in the next game. */ + void getNextRatedGameSettings(int maxLevel, unsigned random, int& level, + Color& color); + + /** Append a new game. */ + void addGame(float score, Rating opponentRating, unsigned nuOpponents, + Color color, float result, const QString& date, int level, + const PentobiTree& tree); + + /** Get file name of the n'th rated game. */ + QString getFile(unsigned n) const; + + void load(Variant variant); + + /** Saves the history. */ + void save() const; + + const vector& getGameInfos() const; + + Variant getVariant() const; + + const Rating& getRating() const; + + const Rating& getBestRating() const; + + unsigned getNuGames() const; + + void clear(); + +private: + Variant m_variant; + + Rating m_rating; + + unsigned m_nuGames; + + Rating m_bestRating; + + QString m_dir; + + QString m_file; + + vector m_games; +}; + +inline const vector& RatingHistory::getGameInfos() + const +{ + return m_games; +} + +inline unsigned RatingHistory::getNuGames() const +{ + return m_nuGames; +} + +inline const Rating& RatingHistory::getBestRating() const +{ + return m_bestRating; +} + +inline const Rating& RatingHistory::getRating() const +{ + return m_rating; +} + +inline Variant RatingHistory::getVariant() const +{ + return m_variant; +} + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_RATING_HISTORY_H diff --git a/src/pentobi/ShowMessage.cpp b/src/pentobi/ShowMessage.cpp new file mode 100644 index 0000000..b8e19f7 --- /dev/null +++ b/src/pentobi/ShowMessage.cpp @@ -0,0 +1,76 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/ShowMessage.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "ShowMessage.h" + +#include +#include "Util.h" + +//----------------------------------------------------------------------------- + +namespace { + +void showMessage(QWidget* parent, QMessageBox::Icon icon, const QString& text, + const QString& infoText, const QString& detailText) +{ + QMessageBox msgBox(parent); + Util::setNoTitle(msgBox); + msgBox.setIcon(icon); + msgBox.setText(text); + msgBox.setInformativeText(infoText); + msgBox.setDetailedText(detailText); + msgBox.exec(); +} + +} // namespace + +//----------------------------------------------------------------------------- + +void initQuestion(QMessageBox& msgBox, const QString& text, + const QString& infoText) +{ + Util::setNoTitle(msgBox); + msgBox.setText(text); + msgBox.setInformativeText(infoText); +} + +void showFatal(const QString& detailedText) +{ + // Don't translate these error messages. They shouldn't occur if the + // program is correct and if it is not, they can occur in situations + // when the translators are not yet installed. + QMessageBox msgBox; + msgBox.setWindowTitle("Pentobi"); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setText("An unexpected error occurred."); + QString infoText = + "Please report this error together with any details available with" + " the button below and other context information at the Pentobi" + " bug tracker."; + msgBox.setInformativeText("" + infoText); + msgBox.setDetailedText(detailedText); + msgBox.exec(); +} + +void showError(QWidget* parent, const QString& text, const QString& infoText, + const QString& detailText) +{ + showMessage(parent,QMessageBox::Critical, text, infoText, detailText); +} + +void showInfo(QWidget* parent, const QString& text, const QString& infoText, + const QString& detailText, bool withIcon) +{ + showMessage(parent, + withIcon ? QMessageBox::Information : QMessageBox::NoIcon, + text, infoText, detailText); +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/ShowMessage.h b/src/pentobi/ShowMessage.h new file mode 100644 index 0000000..3cc3cdc --- /dev/null +++ b/src/pentobi/ShowMessage.h @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/ShowMessage.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_SHOW_MESSAGE_H +#define PENTOBI_SHOW_MESSAGE_H + +#include + +class QMessageBox; +class QWidget; + +//----------------------------------------------------------------------------- + +void initQuestion(QMessageBox& msgBox, const QString& text, + const QString& infoText = ""); + +void showError(QWidget* parent, const QString& text, + const QString& infoText = "", const QString& detailText = ""); + +void showInfo(QWidget* parent, const QString& text, + const QString& infoText = "", const QString& detailText = "", + bool withIcon = false); + +void showFatal(const QString& detailedText); + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_SHOW_MESSAGE_H diff --git a/src/pentobi/Util.cpp b/src/pentobi/Util.cpp new file mode 100644 index 0000000..387b7a2 --- /dev/null +++ b/src/pentobi/Util.cpp @@ -0,0 +1,72 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/Util.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Util.h" + +#include +#include +#include +#include +#include +#include +#include +#include "libpentobi_mcts/Player.h" + +using libpentobi_mcts::Player; + +//----------------------------------------------------------------------------- + +namespace Util +{ + +QString getDataDir() +{ + return QStandardPaths::writableLocation(QStandardPaths::DataLocation); +} + +void initDataDir() +{ + QString dataLocation = getDataDir(); + QDir dir(dataLocation); + if (! dir.exists()) + // Note: dataLocation is an absolute path but there is no static + // function QDir::mkpath() + dir.mkpath(dataLocation); +} + +void removeThumbnail(const QString& file) +{ + // Note: in the future, it might be possible to trigger a thumbnail + // update via D-Bus instead of removing it, but this is not yet + // implemented in Gnome + QFileInfo info(file); + QString canonicalFile = info.canonicalFilePath(); + if (canonicalFile.isEmpty()) + canonicalFile = info.absoluteFilePath(); + QByteArray url = QUrl::fromLocalFile(canonicalFile).toEncoded(); + QByteArray md5 = + QCryptographicHash::hash(url, QCryptographicHash::Md5).toHex(); + QString home = QDir::home().path(); + QFile::remove(home + "/.thumbnails/normal/" + md5 + ".png"); + QFile::remove(home + "/.thumbnails/large/" + md5 + ".png"); +} + +void setNoTitle(QDialog& dialog) +{ + // On many platforms, message boxes should have no title but using + // an emtpy string causes Qt to use the lower-case application name (tested + // on Linux with Qt 4.8). As a workaround, we set the title to a space + // character. + dialog.setWindowTitle(" "); +} + +} // namespace Util + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/Util.h b/src/pentobi/Util.h new file mode 100644 index 0000000..d5ba8cc --- /dev/null +++ b/src/pentobi/Util.h @@ -0,0 +1,49 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/Util.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_UTIL_H +#define PENTOBI_UTIL_H + +#include "RatingHistory.h" +#include "libboardgame_base/Rating.h" +#include "libpentobi_base/Color.h" +#include "libpentobi_base/Variant.h" + +class QDialog; +class QString; + +using libboardgame_base::Rating; +using libpentobi_base::Color; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +namespace Util +{ + +/** Remove a thumbnail for a given file. + Currently, the QT open file dialog shows thumbnails even if they belong + to old versions of a file (see QTBUG-24724). This function can be used + to remove an out-of-date freedesktop.org thumbnail if we know a file has + changed (e.g. after saving). */ +void removeThumbnail(const QString& file); + +/** Return the platform-dependent directory for storing data for the current + application. */ +QString getDataDir(); + +/** Create the platform-dependent directory for storing data for the current + application if it does not exist yet. */ +void initDataDir(); + +/** Set an empty window title for message boxes and similar small dialogs. */ +void setNoTitle(QDialog& dialog); + +} + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_UTIL_H diff --git a/src/pentobi/help/C/pentobi/analysis.jpg b/src/pentobi/help/C/pentobi/analysis.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8731db4d8b46ab37945d83217bc326b380f1aa59 GIT binary patch literal 4625 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 literal 0 HcmV?d00001 diff --git a/src/pentobi/help/C/pentobi/become_stronger.html b/src/pentobi/help/C/pentobi/become_stronger.html new file mode 100644 index 0000000..e1f203f --- /dev/null +++ b/src/pentobi/help/C/pentobi/become_stronger.html @@ -0,0 +1,60 @@ + + + +Pentobi Help + + + + +

Previous | Next

+

Become a Stronger Player

+

Pentobi has functionality that can help you to become a stronger Blokus +player.

+

Game Analysis

+

A game can be analyzed by selecting Analyze Game from the +Tools menu. This will make the computer player evaluate each position in +the main variation. The result is displayed in a window with a diagram of +colored dots.

+

Game analysis window

+
Analysis of a game of variant Classic (2 +players).
+

Each dot represents a game position in which the color of the dot was to +play. The dots are ordered horizontally by move number. The vertical axis +represents the estimated probability of winning the game for the color to play. +Mouse clicks in the diagram will go to the corresponding position.

+

The position values are only estimates and the computer will sometimes +evaluate positions incorrectly. But sudden drops in the value can help you find +moves that were potentially bad. You can go back to the position before the +move and try to find a better move or ask the computer what it would have +played by selecting Play Single Move from the Computer menu.

+

Determine Your Rating

+

You can track your progress by playing rated games against the computer. The +game results are used to determine your current rating. The rating is a number +that represents your playing strength.

+

A rated game is started with Rated Game from the Game menu or +the toolbar. If you have not played any rated games in the current game +variant, you will be asked to choose a start value, which can reduce the number +of games needed for determining your real rating. If you are a beginner, leave +the start value at 1000.

+

For each rated game, the computer will choose a playing level for the +computer opponent according to your current rating. The color you play will be +randomly chosen in each game.

+

During a rated game, most of the functions not needed for playing are +disabled: you cannot undo moves, navigate in the game, change the computer +colors or change the playing level. To get an accurate rating, you should +always play rated games until the end.

+

After the game has ended, your rating will be updated depending on the game +result and the computer level. For the game result, it only matters if the game +was won, lost or a tie. The exact number of score points does not matter.

+

Rating window

+
Window with rating graph.
+

You can always see your current rating by selecting Rating from the +Tools menu. This will open a window that shows the development of your +rating during the last 100 games as a graph. The last 100 games are +automatically saved and can be loaded by double-clicking on the rows in the +game table below the graph.

+

Previous | Next

+ + diff --git a/src/pentobi/help/C/pentobi/board_callisto.png b/src/pentobi/help/C/pentobi/board_callisto.png new file mode 100644 index 0000000000000000000000000000000000000000..f6e63bb10407148e514addadcec7f0871571ba39 GIT binary patch literal 12907 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 literal 0 HcmV?d00001 diff --git a/src/pentobi/help/C/pentobi/board_classic.png b/src/pentobi/help/C/pentobi/board_classic.png new file mode 100644 index 0000000000000000000000000000000000000000..cbbd188350991e1a50bd1294f1425fd3d70a5c10 GIT binary patch literal 1276 zcmah|YfMvD96y&5sdZ2!IzgZ`=*AM*W(L~=q)=H&>2%mBjxY>y7~UYy0V}T-7>dfi zFrsLXj?vzh-acSRK*JahL9o15H*HC^-DI?N#THj3YGLPI9?3o}Ip?0-bN>I|>*O?Y z1bg_x zO}7%xXR&SzyY}y;y&@KfkQ=pi#16BTrD#dao zDpN}2Dil>o&~B-sN380R$VO^-BmBzI8@#c~^0B_M+k>OyeP2%u-JTpCpBlMycXU!c zHnkWP@)sj`DLlvoM?{b*GRUG1w5ShQrUR|h{?-{d0@l~p3qCh3LGMWF5jN}C+4RF4 z07&0*ScenJb=HL=Cp4$M8;pD0DcKkA_=LQh#SdG|z4)n!@7J=jUh`eBQRdrh9vMBQ zWrSji)}s#xP}U8ttu+bLr9F=cdu61TM=X7KOC8mePK_AW_}%;agIm#*e*4{k2SW?d z$TO$YFV5a}kmKd@_Tb`Tii^ZF^MrlQ@p|W>r-N96xFTGb8Y!*u@i=CgJYZNWN=xnN zp{U+5%-a_9pH%FzCAq2&8H~2&S5@H33vuv#u(H6Oxcb<*rr~*2v%W~IVog=DbXG^@ z<4cpp8EWs0H-z0D<|Y&kmuS;UqpzC7>Xw|s?8{nbroGZLYTvWc)~Yv8<`_@3)ttGk zE#?Vn61O!f!;{S91H=U(d(xT7)O#cclQH)@@%7InxspAF?(l%$2a7ENo{-V=C^p|Q zSooX?OZ9@SMmj+7Q#q`u4lunC^wCTnuH?BLIH)h3$>36ZAubp8NxvMdk@C$0|2d;;+!H`TyK;P{W)GJ=N>@`xs$@Mn~eOOre` z3zl(%$c7c%v~Gj+e?>yu(4Q;W6>qW#US8kKzzVHx3)wV^+@xasgF;u?q$iQC~m(m*O`m_Wo2x9ah9xzrN3O)Qwc zkVfRT$+eoO7ib@*?C4yL*Mzwv$$C-C*S(qG|ptdxd=Q;lc%Z-Fw!d++D>{c6~I{lcI3Q;dHoZFcMQgkkS; z@6DW?sKB(Sz-!4ycBf{tN9_|j?n8=$@vQ$w>o3qz+kXm&PsWKYySm!Rxhjo literal 0 HcmV?d00001 diff --git a/src/pentobi/help/C/pentobi/board_duo.png b/src/pentobi/help/C/pentobi/board_duo.png new file mode 100644 index 0000000000000000000000000000000000000000..f41d71b1140933ff2862f11602b67f1306b2c2b2 GIT binary patch literal 660 zcmeAS@N?(olHy`uVBq!ia0vp^4?viM8A$4Wo^=^WF%}28J29*~C-V}>;VkfoEM{Qf zI|9OtQ?>b|fr9KMp1!W^x4ESR7S@up ztovEnO3t$FXN~OL6S+Aerf*Nors%l--Eo^@8>{PE>YCacTRWQCI-A;i+q!$(yBExy zw|LIN#d8-eo4<6;(p77htzNfm&AR2Q*DYVOe#P32tJZH@x@FtSRfl&SJiPnh@%=|n z95{CB(1~-$&iw!XpDF*_1(5TTyxmA$P~4 zA38nQeH0%23xBbtJi%{n^yXP#=Z4ImZpZ(-$nSQG2J_D$~Jzhz7?;3bWXd~ zH9ig~utfvDsR^X{wFWhphn4KVX+h0$#wMg$fPj9(wve@Q6TfBwzp*sXT^tDmCI U$8JkZ1jZACr>mdKI;Vst0Eu%C&;S4c literal 0 HcmV?d00001 diff --git a/src/pentobi/help/C/pentobi/board_nexos.png b/src/pentobi/help/C/pentobi/board_nexos.png new file mode 100644 index 0000000000000000000000000000000000000000..65c55d0b5759b78ada61bc00b448fc89199b6215 GIT binary patch literal 945 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

Zh7T=Te(}hSk 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‡t) zr;LBzkV^g-K@g)uBjp?VJcZHqA6^u*;>I+4pomCuWR0M@OLM$es*Iwi@Rxa7K@nec*^!uQw|FI~8cTfSMRKZCU zQ@@eta@{ZGWyi9}Mq-+hlY1JCNs?w3Q=ysm+@^|a$-~1f29vL!Ml<@*Dk7;)`mhhO zB8cc=K~>1C-^NrW(6Vtj6)|*emm8Ec*la?LrP?fW8-`UHJ-75+w-Q^Pp_%qJk z&naKL5|D%~*WUF|T~4iJ%l63UN?eA-Qi|8Wc`TZ$2&$Nt>AjlTdLLS#g(~5NG@fkO zJTjc^aM2&Mu_$N=7;DgAJ14HP@lo+WtlifJ2DhT`I^?ns!D~Qq ze1z&KXKi3XuUYRDtPyHLj=)=hA@nZQf%{sTleBL$OIP^)Sx?*Ee}d@Wdj4FP^J?#{ zCEsr;b@P?atly%P?X)jOOO5>Vk%c+;OLwViWsdDql8Il|js7k;D`@#9D7C>$mUY(= zDYRPgxEqJ;)8%OstAsK=(rXf!|~ zSS`D+O+y*mfVZmr+vOT0KXdZW$SSfYP6+cv;o9QOlxESGe0xu&>4qp%R-t^4k2h62 z`UPlf?Jp5u@*m-ypDafEzW|qj8Qp6>N)svleiS^+kKd6Z!r!>(Vch+m+O0QU{~Cw3 zeFZ#6@{_`UgDO#+h+GEwr!uPmGN!ebvlRoyF4C z#ko^|GF|*@ELQFDjc)-fg=@F^NvbTOlxA-fBLcO$hsmA7r%KE~olahppSpSURF*o(0Kmq{(Qhve<%O z!!WzBVBuS4(u#wDEE71qN93j48v+uVQmcU~6Q}}*N%&#kznI8ZzRK~cLkoyXYhz#Z z5!ubUgDeK!5QQPJt<&fQv5O_uG1}^}Vzp+AiNSEcbSqJWs#$tG54YTsKv7;-TdZnV#h6dKzOMXs!wBRsr%3uBcQg*BH2L~Ah11gOWrQ+bM zH@AVdZ);YdMg@z>+mvM>wjypP;WjgxC=LauZPGmfc9t=z>Tgswz7>-kGY8-Wr`rGT zjj{kmhBb@aP|Vq)0NHA*42U(v@D@_(;<6W%Y!Kf7e9{uQG0 zg}X{m@-LAIQApw^yi`8InOdAEeo^Wur5z4j@neXZ?h}B0?&rGJoJ|hYBKx7_6U-p{ zaSR-Pc}rT^XZ8Z6>rzk=COhl*81Rt`?Z+y|fX1$ZpWKF5(O06~-v*kc6fzXD_|$$@ z@V+9GfA+(_;x^G}k%P@}#3~0)vuVyFoSy)#3u

4;)liLu7e9p{#>3X_cDxJ23J$ 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 literal 0 HcmV?d00001 diff --git a/src/pentobi/help/C/pentobi/callisto_rules.html b/src/pentobi/help/C/pentobi/callisto_rules.html new file mode 100644 index 0000000..ef5a189 --- /dev/null +++ b/src/pentobi/help/C/pentobi/callisto_rules.html @@ -0,0 +1,45 @@ + + + +Pentobi Help + + + + +

Previous | Next

+

Callisto Rules

+

Callisto is a another board game similar to Blokus. The board is derived +from the classic 20×20 Blokus board by removing the corners such that an +octagon with a top edge of size six remains. The pieces are a subset of the +polyominoes up to size five. They include three 1×1 pieces per player that play +a special role.

+

+"Pieces

+
The 21 pieces.
+

The 1×1 pieces may be placed anywhere on the board apart from the center of +the board. The center consists of an octagon with width six and top edge size +two. The first two moves of a player must use a 1×1 piece, the third 1×1 piece +may be played anytime later.

+

+"Board

+
The board with the center having a darker +color.
+

All larger pieces may be placed anwhere on the board but must touch an +existing piece of the same color edge-to-edge.

+

+"Example

+
An example position after a few +moves.
+

The score of a color is the number of squares on the board occupied by the +color not counting 1×1 pieces. Bonus points are not used. Unlike in Blokus, +ties are broken in favor of the player who started later.

+

Rules for two or three players

+

The game can be played with less than four players by using a smaller board. +For three players, the board is an octagon with width 20 and top edge size two. +For two players, the board is an octagon with width 17 and top edge size two. +The size of the center stays the same.

+

Previous | Next

+ + diff --git a/src/pentobi/help/C/pentobi/classic_rules.html b/src/pentobi/help/C/pentobi/classic_rules.html new file mode 100644 index 0000000..1b6f1c4 --- /dev/null +++ b/src/pentobi/help/C/pentobi/classic_rules.html @@ -0,0 +1,63 @@ + + + +Pentobi Help + + + + +

Previous | Next

+

Classic Rules

+

There are four players, Blue, Yellow, Red and Green, and a board consisting +of 20×20 squares.

+

Each player has a set of 21 pieces of his color shaped like the polyominoes +up to size five. (A polyomino is a shape built by a number of squares connected +along the edges.)

+

+"Pieces

+
The 21 pieces.
+

The players alternate in placing one of their pieces on the board. Blue +starts, followed by Yellow, then Red, then Green.

+

Each player has a starting square. Blue's starting square is in the top left +corner, Yellow's in the top right corner, Red's in the bottom right corner and +Green's in the bottom left corner. The first piece of a player must cover its +starting square.

+

+"Board

+
The 20×20 board with the starting
+squares marked with colored dots.
+

The following pieces must be placed on empty squares such that the new piece +touches at least one piece of its own color corner-to-corner but does not touch +any piece of its own color along the edges. The new piece may touch edges of +pieces of the opponent colors.

+

+"Example

+
An example position after a few +moves.
+

When the player of a color cannot place any more pieces, the player passes +and the next color continues.

+

When none of the players can place any more pieces, the player with the +highest score wins. The score of a color is the number of squares on the board +occupied by the color, plus a bonus of 15 points if the color could place all +of its pieces, plus an additional bonus of 5 points if the color could place +all pieces and the last piece played was the one-square piece.

+

Rules for Two Players

+

The game can be played with two players. The first player plays both Blue +and Red, the second player Yellow and Green. The points of both colors played +by a player are added up.

+

Rules for Three Players

+

The game can also be played with three players. The players take turns +playing the fourth color (Green). At the end of the game, the score of Green is +ignored.

+

Colorless starting points

+

Note that the original Blokus Classic rules used colorless starting points. +This means that each color may freely choose, which of the remaining unoccupied +starting points to use for its first move. Pentobi currently only supports the +rule variant with colored starting points because this rule variant was used on +the Blokus online server at blokus.com and in most of the past Blokus +tournaments.

+

Previous | Next

+ + diff --git a/src/pentobi/help/C/pentobi/duo_rules.html b/src/pentobi/help/C/pentobi/duo_rules.html new file mode 100644 index 0000000..53dba8d --- /dev/null +++ b/src/pentobi/help/C/pentobi/duo_rules.html @@ -0,0 +1,28 @@ + + + +Pentobi Help + + + + +

Previous | Next

+

Duo Rules

+

The game variant Duo is another game variant for two players. The game is +played on a smaller board with 14×14 squares. There is only one color per +player (Blue and Green) and the starting squares are not in the corners, but on +the square with the coordinates (5,10) for Blue, and on (10,5) for Green.

+

+"Board

+
The 14×14 board used in game variant Duo +with
+the starting squares marked with colored dots.
+

+"Example

+
An example position in game variant +Duo.
+

Previous | Next

+ + diff --git a/src/pentobi/help/C/pentobi/index.html b/src/pentobi/help/C/pentobi/index.html new file mode 100644 index 0000000..8e5329d --- /dev/null +++ b/src/pentobi/help/C/pentobi/index.html @@ -0,0 +1,28 @@ + + + +Pentobi Help + + + + +

Next

+

Pentobi

+

Pentobi is a computer opponent for the board game Blokus. In this game, four +players place pieces similar to the pieces of the computer game Tetris on a +20×20 board. Pentobi also supports the game variants for two or three players +and the game variants Duo, Trigon, Junior, Nexos and Callisto.

+

Classic Rules
+Duo Rules
+Trigon Rules
+Junior Rules
+Nexos Rules
+Callisto Rules
+How to Use Pentobi
+Become a Stronger Player
+The Window Menu
+Keyboard Shortcuts
+System Requirements
+License

+ + diff --git a/src/pentobi/help/C/pentobi/junior_rules.html b/src/pentobi/help/C/pentobi/junior_rules.html new file mode 100644 index 0000000..ce2ff38 --- /dev/null +++ b/src/pentobi/help/C/pentobi/junior_rules.html @@ -0,0 +1,22 @@ + + + +Pentobi Help + + + + +

Previous | Next

+

Junior Rules

+

Junior is a simplified game variant for two players. It is played on the +same 14×14 board as game variant Duo but uses only a subset of the pentominoes +and the players get two of each of those pentominoes.

+

+"Pieces

+
The 24 pieces used in Junior.
+

Bonus points are not used in Junior.

+

Previous | Next

+ + diff --git a/src/pentobi/help/C/pentobi/license.html b/src/pentobi/help/C/pentobi/license.html new file mode 100644 index 0000000..6e97dcf --- /dev/null +++ b/src/pentobi/help/C/pentobi/license.html @@ -0,0 +1,26 @@ + + + +Pentobi Help + + + + +

Previous

+

License

+

Copyright © 2011–2017 Markus Enzenberger

+

This program is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the Free +Software Foundation, either version 3 of the License, or (at your option) any +later version.

+

This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details.

+

Trademark Disclaimer

+

The trademark Blokus and other trademarks referred to are property of their +respective trademark holders. The trademark holders are not affiliated with the +author of the program Pentobi.

+

Previous

+ + diff --git a/src/pentobi/help/C/pentobi/nexos_rules.html b/src/pentobi/help/C/pentobi/nexos_rules.html new file mode 100644 index 0000000..5e51e81 --- /dev/null +++ b/src/pentobi/help/C/pentobi/nexos_rules.html @@ -0,0 +1,44 @@ + + + +Pentobi Help + + + + +

Previous | Next

+

Nexos Rules

+

Nexos is a board game similar to Blokus. The board is a rectangular 13×13 +line grid. Each color uses 24 pieces that consist of up to four connected line +segments.

+

Pieces for Nexos

+
The 24 pieces.
+

Each color has a starting intersection on the intersection of the third +lines close to a corner. The first piece must touch the starting +intersection.

+

Board for Nexos

+
The board for Nexos with segments touching +the
+starting intersections marked with colored dots.
+

The following pieces must be placed on empty line segments such that a +segment of the new piece touches an intersection that is already touched by a +segment of the same color. It does not matter if pieces of other colors touch +or cover the same intersection. However, pieces may not overlap. The junctions +between the segments within a piece are such that two rectangular junctions of +different pieces can cover the same intersection without overlapping, but +straight junctions cannot.

+

+"Example

+
An example position after a few +moves.
+

The score of a color is the number of line segments on the board covered by +the color, plus a bonus of 10 points if the color could place all of its +pieces.

+

Rules for Two Players

+

Like Blokus, Nexos can be played with two players by having one player play +Blue and Red and the other player Yellow and Green.

+

Previous | Next

+ + diff --git a/src/pentobi/help/C/pentobi/pieces.png b/src/pentobi/help/C/pentobi/pieces.png new file mode 100644 index 0000000000000000000000000000000000000000..4af286445f1e465925a2ae3888e1243c7abf3ab3 GIT binary patch literal 6338 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 T<9UC+4p3ASG(c5A^AG<47ldyo literal 0 HcmV?d00001 diff --git a/src/pentobi/help/C/pentobi/pieces_callisto.png b/src/pentobi/help/C/pentobi/pieces_callisto.png new file mode 100644 index 0000000000000000000000000000000000000000..976efdcfb0caf7140547076770d046b1f2b761a4 GIT binary patch literal 3898 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 literal 0 HcmV?d00001 diff --git a/src/pentobi/help/C/pentobi/pieces_junior.png b/src/pentobi/help/C/pentobi/pieces_junior.png new file mode 100644 index 0000000000000000000000000000000000000000..6940d2f1b7dd56cb2bcc450fd9c4ae95bbcb880d GIT binary patch literal 7691 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 AX8-^I literal 0 HcmV?d00001 diff --git a/src/pentobi/help/C/pentobi/pieces_nexos.png b/src/pentobi/help/C/pentobi/pieces_nexos.png new file mode 100644 index 0000000000000000000000000000000000000000..9595666e05981c1ba7c0bfd9ee6b9cd8814d8edd GIT binary patch literal 7987 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 literal 0 HcmV?d00001 diff --git a/src/pentobi/help/C/pentobi/pieces_trigon.jpg b/src/pentobi/help/C/pentobi/pieces_trigon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bc9457b842894850082af94cbea306eac388f2f5 GIT binary patch literal 8059 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 literal 0 HcmV?d00001 diff --git a/src/pentobi/help/C/pentobi/position_callisto.png b/src/pentobi/help/C/pentobi/position_callisto.png new file mode 100644 index 0000000000000000000000000000000000000000..1d4d1bdee020854b13d889ac8baf5559d0369a76 GIT binary patch literal 15658 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 K^U|kgAO8pZMh1-l literal 0 HcmV?d00001 diff --git a/src/pentobi/help/C/pentobi/position_nexos.png b/src/pentobi/help/C/pentobi/position_nexos.png new file mode 100644 index 0000000000000000000000000000000000000000..1d66da4e9f2370489991905695cea41045f3e515 GIT binary patch literal 2057 zcmZ{l2~?9;7RO%*8Uh1?6j3RQ(h7AL2xA3A31Jfu2ow;IMgoW{OE3|T21v9ZwpMIw zSt?7UvIt>GM+1f}F``fqa6}*)ARv;k$f6;H5R$LKv7I@m&N=Vh@7#Ot`M&@6JMaGA zHvw2rZO!eP003=oFZVzIV3_r-0asO4v4;y(kJ|yh!5)O4SitK7@VyL3eIONp3;;L) zbOW%Y>H~<>ek2>I$wq4bAlJDL8Kia_v>Ny_eL}CchTaV!FcL0NGr#@%^7p%pu2T&P zJ~J-3$tbAKE4;L&^ahPyQA@vFRaaTVD6eMRu4djSV%FT?UdU)?k{`9TcXA$ek>VaV zo$QT|8S%#m4jmA5I8M0jndtodmD?U6+G@7PTyg4%Vj>8NI|>QI5S{a(D$THSf!H8` z5%Nx~C(%Bh?%?tUV=q;LO!PWJ0zjvIeZzA9u4AMsBF=gHc_7BrG!fe!0twJ_;cN7o|6cxgX~i!cv|vI85^4Tva)Td9`fPPk5{5&257Wi zN68v@AXyNPPyaGyrfDX9gbNd$_uUZYRM!X_!Y10lV%a|n zb?X#qR4i)iqR_2U`CA41O=0nYg=cq8ko_qWjY{H3RZ6UdWoyEwvBJTVe$B*bc0zhX z!>s4*ouvE>ag!4@wAPq;l(pnf$!Jj454(KTN@k}X1u%78%wK{Pe6b>Fv}7%G%07R|r!UJ%Mu1Q8M@>x*pLH!y6XUl%F~y@p zNe0ZfZ zl)Jj2$jis#@p=Yjvc?EIftrRH736{Q&A3suP7{sDUh2o(*BAa%R&#)HhQ;_Qo7#RF zbSPbn3)@d?maj@OlI=q4Z}m_-yR))4NqgJ|Y4Qr2SZ9g+nUH;}ocN%_r%$*lX%({* zcECBKYbh70Flc;q&Hfc7a}&cEy1L=~tP<&%DakHzhO}4dk+Rt}DZvO6pRhj--6-o8 zoKc3#(M;KbIw>kXfj$z!@3S@>ESWmfIJA66zDrFdcwP1V(#_OAm9GDVnnQ+lO;0?N zA1l}YEz2N10V%jU>1WvphH+}LkgcUh!LnI9WGxt^V{mNR@Zp*iAn)17+nzS6p{{;)E(K+8u) zsK)W^L<7rkY+1R!FxcX0gT*8BG=10y&Z@dD2dQ>OEC^;X;(O_Hg?(BC(Zi@&OXB#o zSxHAs-`)ljyQNnTukcELg!@Vu!N)N8h_p74ICW9k!b7S#M7n%tGpaHFkDEI!a>Xyh zQPI|~L6FnJ`OMTPdds~6oWlSz*;e2VJ~Suli5ICMVs1$4*(eqxC1NG5wD$^A!4tD@ zO>(;yJFgq!o3@VVcBb>=CA^er7>>=oQfRnnN)PyMepv5us>H_J49h<>cb`E7cclcKw4 zkG;9@j+8!-ZY%v^Sz=@EeD@h_3(@qn%48CwsT*Zg@b3!x95KZ4F*FPH|G0jSJ0Hnr z;KIn1X`YH_r}+Uq@AY_@i(=gE3e9fL|LNi}u1KrU$z(_a6}M(LF3##Pd4faVgfjkp zW%4eVWKGmV2f?sH#+^y>l~=PSn#$(%(RhbsbZo9xcI_T6{75wINqDhWb}jtu()+&dwSz0Z4d{5h3r zMs1cX>>_Jac9A9~*$M@+S1W(SbR3a)T>9Q(6L(W7!oR&WB%5WUvQhtag|Ob1#OI35 z#PKpkplkUr3oo_I0zd`)@u!B?E9j@MxJ9TBz+YXL%R;3Sqli&~`&4XEyUk<-3Pk0g`FE?*IS* literal 0 HcmV?d00001 diff --git a/src/pentobi/help/C/pentobi/position_trigon.jpg b/src/pentobi/help/C/pentobi/position_trigon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6f6aedc54ec785f89addd991264d2dda33c3c02c GIT binary patch literal 20020 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 F^KZR)pI`t0 literal 0 HcmV?d00001 diff --git a/src/pentobi/help/C/pentobi/rating.jpg b/src/pentobi/help/C/pentobi/rating.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e92aa992c860733234d4f2fa1986dc1372139b7a GIT binary patch literal 7637 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 literal 0 HcmV?d00001 diff --git a/src/pentobi/help/C/pentobi/shortcuts.html b/src/pentobi/help/C/pentobi/shortcuts.html new file mode 100644 index 0000000..ff2e75d --- /dev/null +++ b/src/pentobi/help/C/pentobi/shortcuts.html @@ -0,0 +1,58 @@ + + + +Pentobi Help + + + + +

Previous | Next

+

Keyboard Shortcuts

+

In addition to the menu item shortcut keys, which are shown in the window +menu, the following shortcut keys are supported by Pentobi. Note that these +shortcuts are not active when the comment text field is shown and has the +focus. In this case, the focus can be switched away from the comment text with +the Tab key.

+
+
Plus
+
+

Select next piece

+
+
Minus
+
+

Select previous piece

+
+
0
+
+

Clear selected piece

+
+
Space
+
+

Next orientation of the selected piece

+
+
Shift+Space
+
+

Previous orientation of the selected piece

+
+
Left, Right, Up, Down
+
+

Move the selected piece.

+
+
Enter
+
+

Play the selected piece.

+
+
1, 2, A, C, E, F, G, H, I, J, L, N, O, P, S, T, U, V, W, X, Y, Z
+
+

Select piece according to commonly used piece names. If there are multiple +pieces with the letter (e.g. I3, I4, I5), pressing the key several times cycles +between them. Some letters are used only in certain game variants. For example, +A is used only in Trigon for the pieces A6 and A4 (also known as "lobster" and +"triangle").

+
+
+

Previous | Next

+ + diff --git a/src/pentobi/help/C/pentobi/stylesheet.css b/src/pentobi/help/C/pentobi/stylesheet.css new file mode 100644 index 0000000..62522b0 --- /dev/null +++ b/src/pentobi/help/C/pentobi/stylesheet.css @@ -0,0 +1,20 @@ +body +{ + color: black; + background-color: white; + font-family: sans-serif; + font-size: 15px; + margin-left: 0.5em; + margin-right: 0.5em; + max-width: 60em; +} + +:link +{ + text-decoration: none; +} + +div.caption +{ + font-size: 14px; +} diff --git a/src/pentobi/help/C/pentobi/system.html b/src/pentobi/help/C/pentobi/system.html new file mode 100644 index 0000000..c01c0e4 --- /dev/null +++ b/src/pentobi/help/C/pentobi/system.html @@ -0,0 +1,22 @@ + + + +Pentobi Help + + + + +

Previous | Next

+

System Requirements

+

Minimum: 1 GB RAM, 1 GHz CPU
+Recommended for playing level 9: 4 GB RAM, 2 GHz dual-core or faster +CPU

+

Pentobi will also work on systems that do not meet the minimum requirements +but the highest playing level will be very slow on those systems (if the CPU is +too slow) or have a reduced playing strength (if there is not enough +memory).

+

Previous | Next

+ + diff --git a/src/pentobi/help/C/pentobi/trigon_rules.html b/src/pentobi/help/C/pentobi/trigon_rules.html new file mode 100644 index 0000000..6668322 --- /dev/null +++ b/src/pentobi/help/C/pentobi/trigon_rules.html @@ -0,0 +1,44 @@ + + + +Pentobi Help + + + + +

Previous | Next

+

Trigon Rules

+

Trigon is another game variant. The rules a similar to game variant Classic +but it uses a differently shaped board and a different set of pieces. Each +color uses 22 pieces that are shaped like the polyiamonds up to size six. (A +polyiamond is a shape built by a number of equilateral triangles connected +along the edges.)

+

+"Pieces

+
The 22 Trigon pieces.
+

The board also consists of triangles and is shaped like a hexagon with an +edge size of nine triangles.

+

+"Board

+
The board with the starting
+fields marked with gray dots.
+

There are six starting points on the board, each located in the middle of +the fourth row away from each edge. The starting points are not colored and the +players may freely choose a starting point for the first piece of a color.

+

+"Example

+
An example position after a few +moves.
+

Rules for Two Players

+

Like game variant Classic, Trigon can be played with two players by having +one player play Blue and Red and the other player Yellow and Green.

+

Rules for Three Players

+

Trigon can be played with three players using the same rules as for the +four-player variant. The three-player variant is played on a smaller board with +an edge size of eight triangles. The starting points are located in the middle +of the third row away from each edge.

+

Previous | Next

+ + diff --git a/src/pentobi/help/C/pentobi/user_interface.html b/src/pentobi/help/C/pentobi/user_interface.html new file mode 100644 index 0000000..1e0928d --- /dev/null +++ b/src/pentobi/help/C/pentobi/user_interface.html @@ -0,0 +1,72 @@ + + + +Pentobi Help + + + + +

Previous | Next

+

How to Use Pentobi

+

Board

+

Pentobi's main window shows the board on the left side. The played pieces on +the board can have numbers on them that indicate the move number in which the +piece was played. An letter after the move number indicates that there exists a +variation to this move (see below).

+

Pieces can be played by moving them to a place that corresponds to a legal +move with the mouse or arrow keys and pressing the left mouse button or the +Enter key.

+

Pieces and Score

+

On the right side, the remaining pieces are shown. Above the remaining +pieces is an orientation selector that shows the currently selected piece and +allows the player to change its orientation. If no piece is selected and the +game has not yet ended, a colored dot in the orientation selector shows the +color to play.

+

Pieces can be selected by clicking on one of the remaining pieces shown, by +using the left/right arrow buttons in the orientation selector or by using +shortcut keys.

+

Below the orientation selector is a score display, which displays the +current points for each color or player. The points are the sum of on-board +points and bonus points. Points are underlined if they are final because the +color cannot play more pieces. A small star indicates that the points include a +bonus.

+

Playing Against the Computer

+

The board can be used for creating game records of games played by humans or +for playing games against the computer. In games against the computer, the +computer can play any (or several) of the colors.

+

When you start a new game, the human will play the color(s) of the first +player by default and the computer all other colors. To change this, use +Computer Colors from the Computer menu or toolbar and select the +colors the computer should play.

+

The exception is that the computer will play no color by default if it +played no color in the previous game. This prevents the computer from +automatically starting to play if the user mainly wants to use the board for +entering move sequences or similar editing tasks. So if you want to use the +board without playing against the computer, you need to disable the computer +colors in the Computer Colors dialog only once and it will stay that +way. After loading a saved game, the computer also plays no color by +default.

+

Selecting Play from the Computer menu or the toolbar always +makes the computer play a move for the current color. If the computer did not +already play this color before, it will also make the computer play this color +(and only this color) from now on.

+

Move Variations and the Game Tree

+

When you play a game, Pentobi will store the sequence of moves and it is +always possible to go back to a previous position and play differently. If you +do this, the new sequence is stored as an alternative sequence (called +variation). Variations can also be used by annotators for commenting on +existing games. Variations can exist at any board position and can have +subvariations themselves. The game can therefore become a game tree, in which +each node represents a board position. You can navigate in the game tree with +the items in the Go menu or in the toolbar.

+

The main variation is the sequence of moves that starts at the start +position and always selects the first child node in each position (e.g. by +selecting Forward in the Go menu or toolbar). The main variation +is supposed to represent the real game played. If you want a side variation to +become the main variation, select Make Main Variation from the +Edit menu.

+

Previous | Next

+ + diff --git a/src/pentobi/help/C/pentobi/window_menu.html b/src/pentobi/help/C/pentobi/window_menu.html new file mode 100644 index 0000000..51c56c4 --- /dev/null +++ b/src/pentobi/help/C/pentobi/window_menu.html @@ -0,0 +1,201 @@ + + + +Pentobi Help + + + + +

Previous | Next

+

The Window Menu

+

Game

+
+
New
+
Start a new game.
+
Rated Game
+
Start a new rated game against +the computer.
+
Game Variant
+
Select a game variant and start a new game of this game variant.
+
Game Info
+
Display or edit additional information about the game like the name of the +players or the date when the game was played.
+
Undo Move
+
Undo the last move played and remove it from the game tree. Undoing a move +is only possible if it is the last move in the current variation (i.e. a leaf +node in the game tree; use Edit/Truncate to remove inner nodes of the +game tree).
+
Find Move
+
Find a legal move for the current color and display it for a few seconds on +the board. Selecting this item repeatedly will show all legal moves.
+
Open
+
Load a saved game. The board position after loading will be the last +position in the main variation unless the game starts with a setup position. If +the game starts with a setup, the board position will be the first position +instead. This avoids that solutions are immediately shown if the file contains +a Blokus puzzle as a setup with the solution as the main variation.
+
Open Recent
+
Load a recently used game.
+
Save
+
Save the current game.
+
Save As
+
Save the current game under a new file name.
+
Export/Image
+
Save the current position as an image file. Several image file formats are +supported, the file format is derived from the file name ending (e.g. ".png" +for the PNG format).
+
Export/ASCII Art
+
Save the current position as a text diagram. The text diagram should be +viewed using a monospace font.
+
Quit
+
Quit Pentobi.
+
+

Go

+
+
Beginning
+
Go to the beginning of the game.
+
Backward
+
Go one move backward in the current variation. The corresponding button in +the toolbar supports autorepeat if pressed and held.
+
Forward
+
Go one move forward in the current variation. If the current position has +several follow-up variations (i.e. the current node in the game tree has +several child nodes), the first variation will be used. The corresponding +button in the toolbar supports autorepeat if pressed and held.
+
End
+
Go to the end of the current variation. Like Forward, this also uses +the first variation in positions with several follow-up variations.
+
Next Variation
+
Go to the next variation to the last move played (i.e. the next sibling +node of the current node in the game tree).
+
Previous Variation
+
Go to the previous variation to the last move played (i.e. the previous +sibling node of the current node in the game tree).
+
Go to Move
+
Go to the move with a given move number in the current variation.
+
Back to Main Variation
+
Go back to the last position in the current variation that belonged to the +main variation.
+
Beginning of Branch
+
Go back to the last position in the current variation that had an +alternative move.
+
Find Next Comment
+
Go to the next position that has a comment. If the comment text field was +not visible, it will become visible. Selecting this item repeatedly will show +all positions with comments in the game tree.
+
+

Edit

+
+
Move Annotation
+
Add a chess-style annotation symbol (e.g. !!) to the current move. The +symbols are appended to the move numbers in the status bar and, depending on +the configuration of Move Marking, on the board.
+
Make Main Variation
+
Make the current variation the main variation of the game. This reorders +the nodes in the game tree such that the current variation becomes the main +variation.
+
Move Variation Up
+
Changes the order of variations such that the current position will appear +earlier when iterating over the variations with Next/Previous +Variation.
+
Move Variation Down
+
Changes the order of variations such that the current position will appear +later when iterating over the variations with Next/Previous +Variation.
+
Delete All Variations
+
Delete all variations but the main variation. If the current position is +not in the main variation, it will first be changed to a position as in Back +to Main Variation.
+
Truncate
+
Remove the node with the current position, including any subtree, from the +game tree.
+
Truncate Children
+
Remove all child nodes of the node with the current position from the game +tree.
+
Keep Only Position
+
Delete all moves and keep only the current position as a setup. This can be +used to create files that start with a given fixed position.
+
Keep Only Subtree
+
Like Keep Only Position but does not delete the moves after the +current position.
+
Setup Mode
+
Enter or leave setup mode. In setup mode, pieces can be placed anywhere on +the board, even in violation of the game rules. Existing pieces can be removed +from the board by clicking on them. The currently selected color also +determines the color to play after the setup is finished. It can be changed +with Next Color or by clicking on the orientation selector while no +piece is selected. Setup mode can only be used if no moves have been played +yet.
+
Next Color
+
Choose the next color for selecting pieces. This can be used for example to +enter game records, in which moves of a color were skipped because the color +ran out of time.
+
+

View

+
+
Toolbar
+
Show or hide the toolbar.
+
Toolbar Text
+
Configure the appearance of the toolbar.
+
Comment
+
Show or hide a text field to display or edit comments on the current +position.
+
Move Marking
+
Change the way moves are marked on the board. The options are to mark the +last move played with a dot or with a number, or to show the numbers of all +moves, or not to show any marks.
+
Coordinates
+
Display coordinates around the board for the fields on the board. The +convention for the coordinates is the same as in the Blokus SGF file format +used by Pentobi.
+
Show Variations
+
Appends a letter to the move number on the board if the move has +variations. If moves are marked with dots instead of numbers, a circle will be +used instead of a dot for moves not in the main variation.
+
Fullscreen
+
Make the main window full screen or leave full screen mode. It is +platform-dependent if the window menu is shown in full screen mode. To leave +full screen mode without using the window menu, press the F11 key.
+
+

Computer

+
+
Computer Colors
+
Select which colors are played by the computer.
+
Play
+
Make the computer play a move for the current color. This can be used to +change the color the computer plays or to resume playing after navigating in +the game tree. If the computer did not already play the current color, it will +play this color (and only this color) from now on.
+
Play Single Move
+
Make the computer play a single move for the current color without changing +the colors played by the computer.
+
Stop
+
Abort the current move generation. You can make the computer continue to +play by selecting Play.
+
Level
+
Change the playing strength of the computer. Higher levels are stronger but +can make the computer take a long time for playing moves on slow computers. The +computer will remember the level last used separately for each game variant and +restore it when the game variant is changed.
+
+

Tools

+
+
Rating
+
Show a dialog window with the rating of the user in the current game +variant.
+
Analyze Game
+
Perform a game analysis.
+
+

Help

+
+
Pentobi Help
+
Show a window to browse the Pentobi user manual.
+
About Pentobi
+
Show an info dialog with information about this version of Pentobi.
+
+

Previous | Next

+ + diff --git a/src/pentobi/help/de/pentobi/become_stronger.html b/src/pentobi/help/de/pentobi/become_stronger.html new file mode 100644 index 0000000..6f50443 --- /dev/null +++ b/src/pentobi/help/de/pentobi/become_stronger.html @@ -0,0 +1,67 @@ + + + +Pentobi-Hilfe + + + + +

Zurück | Weiter

+

Ein stärkerer Spieler werden

+

Pentobi besitzt Funktionen, die Ihnen helfen können, ein stärkerer +Blokus-Spieler zu werden.

+

Spielanalyse

+

Sie können ein Spiel analysieren, indem Sie Spiel analysieren aus dem +Extras-Menü wählen. Dies lässt den Computer eine Bewertung jeder +Brettstellung der Hauptvariante ausführen. Das Ergebnis wird in einem Fenster +mit einem Diagramm farbiger Punkte dargestellt.

+

+"Spielanalyse-Fenster"

+
Analyse eines Spiels der Spielvariante +Klassisch (2 Spieler).
+

Jeder Punkt repräsentiert eine Spielstellung, in der die Farbe des Punkts am +Zug war. Die Punkte sind horizontal nach Zugnummer angeordnet. Die vertikale +Achse repräsentiert die Wahrscheinlichkeit, dass die Farbe das Spiel gewinnt. +Mausklicks im Diagramm gehen zur jeweiligen Stellung.

+

Die Werte stellen nur Schätzwerte dar und der Computer wird manchmal +Stellungen nicht korrekt bewerten. Aber ein plötzliches Abfallen des Wertes +kann Ihnen dabei helfen, Züge zu finden, die möglicherweise schlecht waren. Sie +können zur Stellung vor dem Zug zurückgehen und versuchen, einen besseren Zug +zu finden oder den Computer fragen, was er gespielt hätte, indem Sie +Einzelnen Zug spielen aus dem Computer-Menü auswählen.

+

Ihre Wertung ermitteln

+

Sie können Ihre Fortschritte verfolgen, indem Sie gewertete Spiele gegen den +Computer spielen. Die Spielergebnisse werden benutzt, um Ihre gegenwärtige +Wertung zu ermitteln. Die Wertung ist eine Zahl, die Ihre Spielstärke +darstellt.

+

Ein gewertetes Spiel wird mit Gewertetes Spiel aus dem +Spiel-Menü oder der Werkzeugleiste gestartet. Wenn Sie in der +gegenwärtigen Spielvariante noch keine gewerteten Spiele gespielt haben, werden +Sie gefragt, eine Anfangswertung zu wählen, wodurch die Anzahl der Spiele +reduziert wird, die nötig ist, um Ihre wirkliche Wertung zu bestimmen. Falls +Sie Anfänger sind, belassen Sie die Anfangswertung auf 1000.

+

Für jedes gewertete Spiel wird der Computer eine Spielstufe für den +Computerspieler gemäß Ihrer gegenwärtigen Wertung wählen. Die Farbe, die Sie +spielen, wird in jedem Spiel zufällig ausgewählt.

+

Während eines gewerteten Spiels sind die meisten Funktionen, die nicht zum +Spielen benötigt werden, deaktiviert: Sie können keine Züge zurücknehmen, im +Spiel navigieren, die Computer-Farben ändern oder die Spielstufe ändern. Um +eine akkurate Wertung zu erhalten, sollten Sie gewertete Spiele immer bis zum +Ende spielen.

+

Nachdem das Spiel beendet ist, wird Ihre Wertung in Abhängigkeit vom +Spielergebnis und der Spielstufe aktualisiert. Für das Spielergebnis zählt nur, +ob Sie gewonnen oder verloren haben, oder ob das Spiel in einem Unentschieden +endete. Die genaue Anzahl der Spielpunkte spielt keine Rolle.

+

+"Wertungsfenster"

+
Fenster mit Wertungsgraph.
+

Sie können Ihre aktuelle Wertung jederzeit mit Wertung aus dem +Extras-Menü sehen. Dies öffnet ein Fenster, in dem die Entwicklung Ihrer +Wertung während der letzten 100 Spiele als Graph gezeigt wird. Die letzten 100 +Spiele werden automatisch gespeichert und können durch Doppelklick auf die +Zeilen der Spieltabelle unter dem Graph geladen werden.

+

Zurück | Weiter

+ + diff --git a/src/pentobi/help/de/pentobi/callisto_rules.html b/src/pentobi/help/de/pentobi/callisto_rules.html new file mode 100644 index 0000000..8c53e36 --- /dev/null +++ b/src/pentobi/help/de/pentobi/callisto_rules.html @@ -0,0 +1,50 @@ + + + +Pentobi Help + + + + +

Zurück | Weiter

+

Callisto-Regeln

+

Callisto ist ein weiteres Brettspiel ähnlich wie Blokus. Das Spielbrett ist +vom klassischen 20×20-Blokus-Spielbrett abgeleitet, indem die Ecken entfernt +werden, sodass ein Achteck verbleibt mit einer oberen Kantenlänge von sechs. +Die Spielsteine sind eine Untermenge der Polyominos bis zur Größe fünf. Sie +beinhalten drei 1×1-Spielsteine pro Spieler, die eine besondere Rolle +spielen.

+

+"Spielsteine

+
Die 21 Spielsteine.
+

Die 1×1-Spielsteine dürfen überall auf dem Spielbrett gesetzt werden außer +im Zentrum des Spielbretts. Das Zentrums besteht aus einem Achteck mit Breite +sechs und oberer Kantenlänge zwei. Die ersten zwei Züge eines Spielers müssen +einen 1×1-Spielstein benutzen, der dritte 1×1-Spielstein kann jederzeit später +gespielt werden.

+

+"Spielbrett

+
Das Brett mit einer dunkleren Farbe im +Zentrum.
+

Alle größeren Spielsteine dürfen überall auf dem Brett gesetzt werden, +müssen aber einen existierenden Spielstein der selben Farbe Kante an Kante +berühren.

+

+"Beispielstellung

+
Eine Beispielstellung nach ein paar +Zügen.
+

Die Punktzahl einer Farbe ist die Anzahl der Quadrate auf dem Brett, die von +der Farbe bedeckt sind, wobei die 1×1-Spielsteine nicht gezählt werden. +Bonuspunkte werden nicht verwendet. Anders als in Blokus werden Unentschieden +zugunsten des Spielers aufgelöst, der später begonnen hat.

+

Regeln für zwei oder drei Spieler

+

Das Spiel kann mit weniger als vier Spielern gespielt werden, indem ein +kleineres Spielbrett verwendet wird. Für drei Spieler ist das Brett ein Achteck +mit Breite 20 und obererer Kantenlänge zwei. Für zwei Spieler ist das Brett ein +Achteck mit Breite 16 und obererer Kantenlänge zwei. Die Größe des Zentrums +bleibt gleich.

+

Zurück | Weiter

+ + diff --git a/src/pentobi/help/de/pentobi/classic_rules.html b/src/pentobi/help/de/pentobi/classic_rules.html new file mode 100644 index 0000000..fefcf6d --- /dev/null +++ b/src/pentobi/help/de/pentobi/classic_rules.html @@ -0,0 +1,65 @@ + + + +Pentobi-Hilfe + + + + +

Zurück | Weiter

+

Klassische Regeln

+

Es gibt vier Spieler, Blau, Gelb, Rot und Grün, und ein Brett, das aus 20×20 +Quadraten besteht.

+

Jeder Spieler besitzt 21 Spielsteine seiner Farbe, die die Form von +Polyominos bis zur Größe fünf haben (ein Polyomino ist eine Figur, die aus +einer Anzahl von Quadraten besteht, die entlang der Kanten verbunden sind).

+

+"Spielsteine

+
Die 21 Spielsteine.
+

Die Spieler setzen abwechselnd einen ihrer Spielsteine aufs Brett. Blau +fängt an, gefolgt von Gelb, dann Rot, dann Grün.

+

Jeder Spieler hat ein Startfeld. Das Startfeld von Blau ist in der oberen +linken Ecke, das von Gelb in der oberen rechten Ecke, das von Rot in der +unteren rechten Ecke und das von Grün in der unteren linken Ecke. Der erste +Spielstein eines Spielers muss sein Startfeld abdecken.

+

+"Spielbrett

+
Das 20×20-Brett mit den Startfeldern
+durch farbige Punkte markiert.
+

Die folgenden Spielsteine müssen so auf leere Quadrate gesetzt werden, dass +der neue Spielstein mindestens einen Spielstein der eigenen Farbe Ecke an Ecke +berührt, aber keinen Spielstein der eigenen Farbe entlang der Kanten. Der neue +Spielstein darf die Kanten von gegnerischen Spielsteinen berühren.

+

+"Beispielstellung

+
Eine Beispielstellung nach ein paar +Zügen.
+

Wenn der Spieler einer Farbe keine Spielsteine mehr setzen kann, muss der +Spieler aussetzen und die nächste Farbe ist am Zug.

+

Wenn keiner der Spieler mehr einen Spielstein setzen kann, gewinnt der +Spieler mit der höchsten Punktzahl. Die Punktzahl einer Farbe ist die Anzahl +der Quadrate auf dem Brett, die von der Farbe besetzt sind, plus ein Bonus von +15 Punkten, wenn die Farbe alle ihre Spielsteine setzen konnte, plus ein +zusätzlicher Bonus von 5 Punkten, wenn die Farbe alle Spielsteine setzen konnte +und der zuletzt gespielte Spielstein der Spielstein war, der aus einem Quadrat +besteht.

+

Regeln für zwei Spieler

+

Das Spiel kann mit zwei Spielern gespielt werden. Der erste Spieler spielt +Blau und Rot, der zweite Spieler Gelb und Grün. Die Punkte von beiden Farben +eines Spielers werden addiert.

+

Regeln für drei Spieler

+

Das Spiel kann auch mit drei Spielern gespielt werden. Die Spieler wechseln +sich beim Spielen der vierten Farbe (Grün) ab. Am Spielende wird die Punktzahl +von Grün ignoriert.

+

Farblose Startfelder

+

Beachten Sie, dass die ursprünglichen klassischen Regeln für Blokus farblose +Startfelder benutzen. Dies bedeutet, dass jede Farbe frei wählen darf, welches +der verbleibenden noch freien Startfelder sie für ihren ersten Zug benutzt. +Pentobi unterstützt zur Zeit nur die Regelvariante mit farbigen Startfeldern, +weil diese Variante auf dem Blokus-Online-Server auf blokus.com und in den +meisten bisherigen Blokus-Turnieren verwendet wurde.

+

Zurück | Weiter

+ + diff --git a/src/pentobi/help/de/pentobi/duo_rules.html b/src/pentobi/help/de/pentobi/duo_rules.html new file mode 100644 index 0000000..bdc1318 --- /dev/null +++ b/src/pentobi/help/de/pentobi/duo_rules.html @@ -0,0 +1,29 @@ + + + +Pentobi-Hilfe + + + + +

Zurück | Weiter

+

Duo-Regeln

+

Die Spielvariante Duo ist eine andere Spielvariante für zwei Spieler. Das +Spiel wird auf einem kleineren Brett mit 14×14 Quadraten gespielt. Es gibt eine +Farbe pro Spieler (Blau und Grün) und die Startfelder befinden sich nicht in +den Ecken, sondern auf dem Feld mit den Koordinaten (5,10) für Blau und auf +(10,5) für Grün.

+

+"Spielbrett

+
Das 14×14-Brett, das in der Spielvariante +Duo benutzt
+wird, mit den Startfeldern durch farbige Punkte markiert.
+

+"Beispielstellung

+
Eine Beispielstellung in der Spielvariante +Duo.
+

Zurück | Weiter

+ + diff --git a/src/pentobi/help/de/pentobi/index.html b/src/pentobi/help/de/pentobi/index.html new file mode 100644 index 0000000..6d22c49 --- /dev/null +++ b/src/pentobi/help/de/pentobi/index.html @@ -0,0 +1,29 @@ + + + +Pentobi-Hilfe + + + + +

Weiter

+

Pentobi

+

Pentobi ist ein Computer-Gegner für das Brettspiel Blokus. In diesem Spiel +setzen vier Spieler Spielsteine, die ähnlich den Spielsteinen des +Computerspiels Tetris sind, auf ein 20×20-Brett. Pentobi unterstützt auch die +Spielvarianten für zwei oder drei Spieler und die Spielvarianten Duo, Trigon, +Junior, Nexos und Callisto.

+

Klassische Regeln
+Duo-Regeln
+Trigon-Regeln
+Junior-Regeln
+Nexos-Regeln
+Callisto-Regeln
+Wie Sie Pentobi benutzen
+Ein stärkerer Spieler werden
+Das Fenstermenü
+Tastenkürzel
+Systemvoraussetzungen
+Lizenz

+ + diff --git a/src/pentobi/help/de/pentobi/junior_rules.html b/src/pentobi/help/de/pentobi/junior_rules.html new file mode 100644 index 0000000..15f9ba9 --- /dev/null +++ b/src/pentobi/help/de/pentobi/junior_rules.html @@ -0,0 +1,24 @@ + + + +Pentobi-Hilfe + + + + +

Zurück | Weiter

+

Junior-Regeln

+

Junior ist eine vereinfachte Spielvariante für zwei Spieler. Es wird auf dem +gleichen 14×14-Brett gespielt wie die Spielvariante Duo, benutzt aber nur eine +Teilmenge der Pentominos und die Spieler bekommen zwei von jedem dieser +Pentominos.

+

+"Spielsteine

+
Die 24 Spielsteine, die in Junior benutzt +werden.
+

Bonuspunkte werden in Junior nicht benutzt.

+

Zurück | Weiter

+ + diff --git a/src/pentobi/help/de/pentobi/license.html b/src/pentobi/help/de/pentobi/license.html new file mode 100644 index 0000000..e4e2b74 --- /dev/null +++ b/src/pentobi/help/de/pentobi/license.html @@ -0,0 +1,26 @@ + + + +Pentobi-Hilfe + + + + +

Zurück

+

Lizenz

+

Copyright © 2011–2017 Markus Enzenberger

+

Dieses Programm ist freie Software. Sie können es unter den Bedingungen der +GNU General Public License, wie von der Free Software Foundation +veröffentlicht, weitergeben und/oder modifizieren, entweder gemäß Version 3 der +Lizenz oder (nach Ihrer Wahl) jeder späteren Version.

+

Die Veröffentlichung dieses Programms erfolgt in der Hoffnung, dass es Ihnen +von Nutzen sein wird, aber OHNE IRGENDEINE GARANTIE, insbesondere ohne eine +implizite Garantie der MARKTREIFE oder der VERWENDBARKEIT FÜR EINEN BESTIMMTEN +ZWECK. Nähere Angaben finden Sie in der GNU General Public License.

+

Hinweis zu Markennamen

+

Der Markenname Blokus und andere erwähnte Marken sind Eigentum ihrer +jeweiligen Markeninhaber. Die Markeninhaber stehen in keiner Verbindung mit dem +Autor des Programms Pentobi.

+

Zurück

+ + diff --git a/src/pentobi/help/de/pentobi/nexos_rules.html b/src/pentobi/help/de/pentobi/nexos_rules.html new file mode 100644 index 0000000..84d0e1d --- /dev/null +++ b/src/pentobi/help/de/pentobi/nexos_rules.html @@ -0,0 +1,47 @@ + + + +Pentobi Help + + + + +

Zurück | Weiter

+

Nexos-Regeln

+

Nexos ist ein Brettspiel ähnlich wie Blokus. Das Spielbrett ist ein +rechtwinkliges 13×13-Liniengitter. Jede Farbe benutzt 24 Spielsteine, die aus +bis zu vier verbundenen Liniensegmenten bestehen.

+

+"Spielsteine

+
Die 24 Spielsteine.
+

Jede Farbe hat einen Startkreuzungspunkt auf der Kreuzung der dritten Linien +nahe einer Ecke. Der erste Spielstein muss den Startkreuzungspunkt +berühren.

+

+"Spielbrett

+
Das Brett für Nexos mit den die +Startkreuzungspunkte
+berührenden Segmenten durch farbige Punkte markiert.
+

Die folgenden Spielsteine müssen so auf leere Liniensegmente gesetzt werden, +dass ein Segment des neuen Spielsteins einen Kreuzungspunkt berührt, den +bereits ein Segment derselben Farbe berührt. Es spielt keine Rolle, ob +Spielsteine anderer Farbe denselben Kreuzungspunkt berühren oder bedecken. +Allerdings dürfen sich Spielsteine nicht überlappen. Die Verbindungen zwischen +den Segmenten innerhalb eines Spielsteins sind so, dass zwei rechtwinklige +Verbindungen verschiedener Spielsteine denselben Kreuzungspunkt bedecken können +ohne sich zu überlappen, während gerade Verbindungen das nicht können.

+

+"Beispielstellung

+
Eine Beispielstellung nach ein paar +Zügen.
+

Die Punktzahl einer Farbe ist die Anzahl der Liniensegmente auf dem Brett, +die von der Farbe bedeckt sind, plus ein Bonus von 10 Punkten, wenn die Farbe +alle ihre Spielsteine setzen konnte.

+

Regeln für zwei Spieler

+

Wie Blokus kann Nexos von zwei Spielern gespielt werden, indem ein Spieler +Rot und Blau, und der andere Spieler Gelb und Grün spielt.

+

Zurück | Weiter

+ + diff --git a/src/pentobi/help/de/pentobi/shortcuts.html b/src/pentobi/help/de/pentobi/shortcuts.html new file mode 100644 index 0000000..3805109 --- /dev/null +++ b/src/pentobi/help/de/pentobi/shortcuts.html @@ -0,0 +1,59 @@ + + + +Pentobi-Hilfe + + + + +

Zurück | Weiter

+

Tastenkürzel

+

Zusätzlich zu den Tastenkürzeln der Menüpunkte, die im Fenstermenü gezeigt +werden, werden die folgenden weiteren Tastenkürzel von Pentobi unterstützt. +Beachten Sie, dass diese Tastenkürzel nicht aktiv sind, wenn das Kommentarfeld +sichtbar ist und den Fokus besitzt. In diesem Fall kann der Fokus vom +Kommentartext durch die Tabulator-Taste entfernt werden.

+
+
Plus
+
+

Nächsten Spielstein auswählen

+
+
Minus
+
+

Vorherigen Spielstein auswählen

+
+
0
+
+

Spielsteinauswahl löschen

+
+
Leertaste
+
+

Nächste Ausrichtung des ausgewählten Spielsteins

+
+
Umschalt+Leertaste
+
+

Vorherige Ausrichtung des ausgewählten Spielsteins

+
+
Links, Rechts, Oben, Unten
+
+

Bewegen des ausgewählten Spielsteins.

+
+
Enter
+
+

Spielen des ausgewählten Spielsteins.

+
+
1, 2, A, C, E, F, G, H, I, J, L, N, O, P, S, T, U, V, W, X, Y, Z
+
+

Einen Spielstein entsprechend den üblicherweise benutzten Spielsteinnamen +auswählen. Wenn es mehrere Spielsteine mit dem Buchstaben gibt (z. B. I3, +I4, I5), dann kann durch mehrmaliges Drücken der Taste zwischen ihnen +gewechselt werden. Einige Buchstaben werden nur in bestimmten Spielvarianten +benutzt. Zum Beispiel wird A nur in Trigon für die Spielsteine A6 und A4 +benutzt (auch bekannt als „Hummer“ und „Dreieck“).

+
+
+

Zurück | Weiter

+ + diff --git a/src/pentobi/help/de/pentobi/system.html b/src/pentobi/help/de/pentobi/system.html new file mode 100644 index 0000000..26ed70c --- /dev/null +++ b/src/pentobi/help/de/pentobi/system.html @@ -0,0 +1,22 @@ + + + +Pentobi-Hilfe + + + + +

Zurück | Weiter

+

Systemvoraussetzungen

+

Minimum: 1 GB RAM, 1 GHz CPU
+Empfohlen für Spielstufe 9: 4 GB RAM, 2 GHz Dual-Core- oder schnellere +CPU

+

Pentobi funktioniert auch auf Systemen, die das Systemminimum nicht +erfüllen, aber die höchste Spielstufe kann auf diesen Systemen sehr langsam +sein (wenn die CPU zu langsam ist) oder eine reduzierte Spielstärke haben (wenn +nicht genügend Arbeitsspeicher vorhanden ist).

+

Zurück | Weiter

+ + diff --git a/src/pentobi/help/de/pentobi/trigon_rules.html b/src/pentobi/help/de/pentobi/trigon_rules.html new file mode 100644 index 0000000..d4b99cf --- /dev/null +++ b/src/pentobi/help/de/pentobi/trigon_rules.html @@ -0,0 +1,47 @@ + + + +Pentobi-Hilfe + + + + +

Zurück | Weiter

+

Trigon-Regeln

+

Trigon ist eine weitere Spielvariante. Die Regeln sind ähnlich wie in der +Spielvariante Klassisch, aber es werden ein anders geformtes Brett und andere +Spielsteine verwendet. Jede Farbe benutzt 22 Spielsteine, die wie die +Polyiamonds bis zur Größe sechs geformt sind (ein Polyiamond ist eine Figur, +die aus einer Anzahl von gleichseitigen Dreiecken besteht, die entlang der +Kanten verbunden sind).

+

+"Spielsteine

+
Die 22 Trigon-Spielsteine.
+

Das Spielbrett besteht ebenfalls aus Dreiecken und hat die Form eines +Sechsecks mit jeweils neun Dreiecken pro Kante.

+

+"Spielbrett

+
Das Brett mit den Startfeldern
+durch graue Punkte markiert.
+

Es gibt sechs Startfelder auf dem Brett, jedes in der Mitte der vierten +Reihe von jeder Kante aus gesehen. Die Startfelder sind nicht farbig und die +Spieler dürfen das Startfeld für den ersten Spielstein einer Farbe frei +wählen.

+

+"Beispielstellung

+
Eine Beispielstellung nach ein paar +Zügen.
+

Regeln für zwei Spieler

+

Wie die Spielvariante Klassisch kann Trigon mit zwei Spielern gespielt +werden, indem ein Spieler Blau und Rot und der andere Gelb und Grün spielt.

+

Regeln für drei Spieler

+

Trigon kann mit drei Spielern gespielt werden, wobei dieselben Regeln wie +für die Variante mit vier Spielern benutzt werden. Die Variante für drei +Spieler wird auf einem kleineren Spielbrett mit einer Kantenlänge von acht +Dreiecken gespielt. Die Startfelder sind in der Mitte der dritten Reihe von +jeder Kante aus gesehen.

+

Zurück | Weiter

+ + diff --git a/src/pentobi/help/de/pentobi/user_interface.html b/src/pentobi/help/de/pentobi/user_interface.html new file mode 100644 index 0000000..6a5dc95 --- /dev/null +++ b/src/pentobi/help/de/pentobi/user_interface.html @@ -0,0 +1,80 @@ + + + +Pentobi-Hilfe + + + + +

Zurück | Weiter

+

Wie Sie Pentobi benutzen

+

Spielbrett

+

Pentobis Hauptfenster zeigt das Spielbrett auf der linken Seite. Auf den +gespielten Spielsteinen auf dem Brett können sich Nummern befinden, die die +Zugnummer angeben, zu der der Spielstein gespielt wurde. Ein Buchstabe nach der +Zugnummer zeigt an, dass zu diesem Zug eine Variante existiert (siehe +unten).

+

Spielsteine können gespielt werden, indem sie mit der Maus oder den +Pfeiltasten an eine Position gebracht werden, die einem legalen Zug entspricht, +und dann die linke Maustaste oder die Eingabetaste gedrückt wird.

+

Spielsteine und Punkte

+

Auf der rechten Seite werden die verbleibenden Spielsteine gezeigt. Über den +verbleibenden Spielsteinen befinden sich eine Orientierungsauswahl, die den +ausgewählten Spielstein zeigt und es dem Spieler erlaubt, seine Orientierung zu +ändern. Wenn kein Spielstein ausgewählt ist und das Spiel noch nicht beendet +ist, zeigt ein farbiger Punkt in der Orientierungsauswahl, welche Farbe am Zug +ist.

+

Spielsteine können durch Klicken auf einen gezeigten verbleibenden +Spielstein ausgewählt werden, durch Benutzen der Buttons mit dem +Links/Rechts-Pfeil in der Orientierungsauswahl oder durch Benutzen von Tastenkürzeln.

+

Unterhalb der Orientierungsauswahl befindet sich eine Punkteanzeige, die die +gegenwärtigen Punkte für jede Farbe oder jeden Spieler zeigt. Die Punkte sind +die Summe aus den Punkten auf dem Spielbrett und den Bonuspunkten. Punkte sind +unterstrichen, wenn sie endgültig sind, weil die Farbe keine Spielsteine mehr +spielen kann. Eine kleiner Stern zeigt an, dass die Punkte einen Bonus +beinhalten.

+

Gegen den Computer spielen

+

Das Spielbrett kann benutzt werden, um Partien einzugeben, die von Menschen +gespielt werden, oder um Spiele gegen den Computer zu spielen. In Spielen gegen +den Computer kann der Computer jede der Farben (oder mehrere) spielen.

+

Wenn Sie ein neues Spiel beginnen, ist voreingestellt, dass der Mensch die +Farbe(n) des ersten Spielers spielt und der Computer alle anderen Farben. Um +dies zu ändern, benutzen Sie Computer-Farben aus dem Menü +Computer oder der Werkzeugleiste und wählen Sie die Farben, die der +Computer spielen soll.

+

Die Ausnahme ist, dass es voreingestellt ist, dass der Computer keine Farbe +spielt, wenn er im letzten Spiel keine Farbe gespielt hat. Damit wird +vermieden, dass der Computer unbeabsichtigt automatisch zu spielen beginnt, +wenn der Benutzer das Spielbrett hauptsächlich zum Eingeben von Zugsequenzen +oder ähnliche Aufgaben benutzen will. Wenn Sie also das Spielbrett benutzen +wollen ohne gegen den Computer zu spielen, brauchen Sie nur einmal die Farben +des Computers im Dialogfenster Computer-Farben abschalten und diese +Einstellung wird sich nicht ändern. Nach dem Laden eines Spiels ist ebenfalls +voreingestellt, dass der Computer keine Farbe spielt.

+

Die Auswahl von Spielen aus dem Menü Computer oder der +Werkzeugleiste lässt den Computer immer einen Zug für die gegenwärtige Farbe +spielen. Wenn der Computer diese Farbe bisher nicht gespielt hat, wird er +außerdem im weiteren Spielverlauf diese Farbe (und nur diese Farbe) +spielen.

+

Zugvarianten und der Spielbaum

+

Wenn Sie ein Spiel spielen, wird Pentobi die Abfolge der Züge speichern und +es ist jederzeit möglich, zu einer früheren Brettstellung zurückzugehen und +anders zu spielen. Wenn Sie das tun, wird die neue Zugfolgen als eine +alternative Zugfolge (genannt Variante) gespeichert. Varianten können auch von +Kommentatoren benutzt werden, um Kommentierungen zu existierenden Spielen +hinzuzufügen. Varianten können in jeder Brettstellung existieren und ihrerseits +Untervarianten besitzen. Das Spiel kann daher zu einem Spielbaum werden, in dem +jeder Knoten eine Brettstellung repräsentiert. Sie können im Spielbaum mit den +Menüpunkten des Menüs Gehe zu oder der Werkzeugleiste navigieren.

+

Die Hauptvariante ist die Zugfolge, die in der Startstellung beginnt und +immer den ersten Kindknoten in jeder Brettstellung wählt (z. B. indem Sie +Vorwärts im Menü Gehe zu oder der Werkzeugleiste benutzen). Die +Hauptvariante sollte das wirklich gespielte Spiel darstellen. Wenn Sie eine +Nebenvariante zur Hauptvariante machen wollen, wählen Sie Zu Hauptvariante +machen aus dem Bearbeiten-Menü.

+

Zurück | Weiter

+ + diff --git a/src/pentobi/help/de/pentobi/window_menu.html b/src/pentobi/help/de/pentobi/window_menu.html new file mode 100644 index 0000000..e861dbe --- /dev/null +++ b/src/pentobi/help/de/pentobi/window_menu.html @@ -0,0 +1,226 @@ + + + +Pentobi-Hilfe + + + + +

Zurück | Weiter

+

Das Fenstermenü

+

Spiel

+
+
Neu
+
Beginnt ein neues Spiel.
+
Gewertetes Spiel
+
Beginnt ein neues gewertetes +Spiel gegen den Computer.
+
Spielvariante
+
Wählt eine Spielvariante und beginnt ein neues Spiel dieser +Spielvariante.
+
Spielinformation
+
Öffnet ein Dialogfenster zum Anzeigen oder Bearbeiten zusätzlicher +Informationen über das Spiel, wie die Namen der Spieler oder das Datum, an dem +das Spiel gespielt wurde.
+
Zug rückgängig
+
Nimmt den zuletzt gespielten Zug zurück und entfernt ihn aus dem Spielbaum. +Das Zurücknehmen eines Zugs ist nur möglich, wenn er der letzte Zug der +gegenwärtigen Variante ist (d. h. ein Endknoten im Spielbaum; benutzen Sie +Bearbeiten/Abschneiden zum Entfernen innerer Knoten aus dem +Spielbaum).
+
Zug finden
+
Findet einen legalen Zug für die gegenwärtige Farbe und zeigt ihn für ein +paar Sekunden auf dem Spielbrett. Das wiederholte Auswählen dieses Menüpunkts +zeigt alle legalen Züge.
+
Öffnen
+
Lädt ein gespeichertes Spiel. Die Brettstellung nach dem Laden ist die +letzte Stellung in der Hauptvariante, sofern das Spiel nicht mit einer +aufgebauten Brettstellung beginnt. Wenn das Spiel mit einer aufgebauten +Brettstellung beginnt, ist die Stellung nach dem Laden stattdessen die +Anfangsstellung. Dies vermeidet, dass Lösungen sofort angezeigt werden, wenn +die Datei ein Blokus-Problem als aufgebaute Brettstellung enthält mit der +Lösung in der Hauptvariante.
+
Zuletzt benutzte Dateien
+
Lädt ein kürzlich benutztes Spiel.
+
Speichern
+
Speichert das gegenwärtige Spiel.
+
Speichern unter
+
Speichert das gegenwärtige Spiel unter einem neuen Dateinamen.
+
Exportieren/Grafik
+
Speichert die gegenwärtige Brettstellung als eine Grafikdatei. Mehrere +Grafikdateiformate werden unterstützt, das Dateiformat wird von der Dateiendung +abgeleitet (z. B. „.png“ für das PNG-Format).
+
Exportieren/ASCII-Art
+
Speichert die gegenwärtige Brettstellung als Textdiagramm. Das Textdiagramm +sollte mit einer Schriftart fester Breite betrachtet werden.
+
Beenden
+
Beendet Pentobi.
+
+

Gehe zu

+
+
Anfang
+
Geht zum Anfang des Spiels.
+
Zurück
+
Geht einen Zug in der gegenwärtigen Variante zurück. Der entsprechende +Button in der Werkzeugleiste unterstützt automatische Wiederholung, wenn er +gedrückt gehalten wird.
+
Vorwärts
+
Geht einen Zug in der gegenwärtigen Variante vorwärts. Wenn die +gegenwärtige Brettstellung mehrerer nachfolgende Varianten hat (d. h. der +gegenwärtige Knoten im Spielbaum mehrere Kindknoten hat), wird die erste +Variante benutzt. Der entsprechende Button in der Werkzeugleiste unterstützt +automatische Wiederholung, wenn er gedrückt gehalten wird.
+
Ende
+
Geht zum Ende der gegenwärtigen Variante. Wie bei Vorwärts wird auch +hier jeweils die erste Variante benutzt, wenn die Brettstellung mehrere +nachfolgende Varianten hat.
+
Nächste Variante
+
Geht zur nächsten Variante zum zuletzt gespielten Zug (d. h. zum +nächsten Geschwisterknoten des gegenwärtigen Knotens im Spielbaum).
+
Vorherige Variante
+
Geht zur vorherigen Variante zum zuletzt gespielten Zug (d. h. zum +vorherigen Geschwisterknoten des gegenwärtigen Knotens im Spielbaum).
+
Gehe zu Zug
+
Geht zum Zug mit einer bestimmten Nummer in der gegenwärtigen +Variante.
+
Zurück zu Hauptvariante
+
Kehrt zur letzten Brettstellung in der gegenwärtigen Variante zurück, die +zur Hauptvariante gehörte.
+
Anfang der Verzweigung
+
Kehrt zur letzten Brettstellung in der gegenwärtigen Variante zurück, die +einen alternativen Zug hatte.
+
Nächsten Kommentar finden
+
Geht zur nächsten Brettstellung, die einen Kommentar besitzt. Wenn das +Kommentarfeld nicht sichtbar ist, wird es sichtbar gemacht. Das wiederholte +Auswählen dieses Menüpunkts zeigt nacheinander alle Brettstellungen mit +Kommentaren im Spielbaum.
+
+

Bearbeiten

+
+
Zugkommentierung
+
Fügt ein wie in der Schachnotation benutztes Symbol (z. B. !!) zum +gegenwärtigen Zug hinzu. Die Symbole werden an die Zugnummern in der +Statusleiste angehängt und, abhängig von der Einstellung von +Zugmarkierung, an die auf dem Spielbrett.
+
Zu Hauptvariante machen
+
Macht die gegenwärtige Variante zur Hauptvariante des Spiels. Dies ordnet +die Knoten im Spielbaum so um, dass die gegenwärtige Variante zur Hauptvariante +wird.
+
Variante nach oben schieben
+
Ändert die Reihenfolge der Varianten so, dass die gegenwärtige +Brettstellung beim Durchlaufen der Varianten mit Nächste/Vorherige +Variante früher erscheint.
+
Variante nach unten schieben
+
Ändert die Reihenfolge der Varianten so, dass die gegenwärtige +Brettstellung beim Durchlaufen der Varianten mit Nächste/Vorherige +Variante später erscheint.
+
Alle Varianten löschen
+
Löscht alle Varianten außer der Hauptvariante. Wenn sich die gegenwärtige +Brettstellung nicht in der Hauptvariante befindet, wird zuvor zu einer +Brettstellung in der Hauptvariante gewechselt wie in Zurück zu +Hauptvariante.
+
Abschneiden
+
Entfernt den Knoten mit der gegenwärtigen Brettstellung zusammen mit dem +auf ihn folgenden Teilbaum aus dem Spielbaum.
+
Kindknoten abschneiden
+
Entfernt alle Kindknoten des Knotens mit der gegenwärtigen Brettstellung +aus dem Spielbaum.
+
Nur Brettstellung behalten
+
Löscht alle Züge und behält nur die gegenwärtige Brettstellung als feste +Stellung. Dies kann zur Erzeugung von Dateien benutzt werden, die mit einer +festgelegten Brettstellung beginnen.
+
Nur Teilbaum behalten
+
Wie Nur Brettstellung behalten, aber die Züge nach der gegenwärtigen +Brettstellung werden nicht gelöscht.
+
Stellungsaufbau
+
Aktiviert oder deaktiviert den Stellungsaufbau-Modus. Im +Stellungsaufbau-Modus können Spielsteine überall auf dem Brett abgelegt werden, +auch unter Verletzung der Spielregeln. Existierende Spielsteine können durch +Anklicken vom Brett entfernt werden. Die gegenwärtig gewählte Farbe legt auch +die Farbe fest, die nach Beenden des Stellungsaufbaus am Zug ist. Sie kann mit +Nächste Farbe oder durch Klicken auf die Orientierungsauswahl während +kein Spielstein ausgewählt ist geändert werden. Der Stellungsaufbau-Modus kann +nur benutzt werden, wenn noch keine Züge gespielt wurden.
+
Nächste Farbe
+
Wählt die nächste Farbe zum Auswählen eines Spielsteins. Dies kann zum +Beispiel benutzt werden, um Partien einzugeben, bei denen Züge einer Farbe +übersprungen wurden, da die Farbe aufgrund einer Bedenkzeitüberschreitung vom +Weiterspielen ausgeschlossen wurde.
+
+

Ansicht

+
+
Werkzeugleiste
+
Zeigt oder verbirgt die Werkzeugleiste.
+
Werkzeugleistentext
+
Konfiguriert das Aussehen der Werkzeugleiste.
+
Kommentar
+
Zeigt oder verbirgt ein Textfeld zum Anzeigen oder Bearbeiten von +Kommentaren zur gegenwärtigen Brettstellung.
+
Zugnummern
+
Ändert die Anzeigeart von Zugnummern auf dem Spielbrett. Die Optionen sind +nur die Nummer des zuletzt gespielten Zugs zu zeigen (oder der zuletzt +gespielten Züge, wenn der Computer mehrere Züge nacheinander spielt) oder die +Nummern aller Züge zu zeigen oder gar keine Nummern zu zeigen.
+
Zugmarkierung
+
Ändert die Markierung von Zügen auf dem Spielbrett. Die Optionen sind, den +zuletzt gespielten Zug mit einem Punkt oder einer Nummer zu markieren, oder die +Nummern aller Züge zu zeigen oder gar keine Markierung zu zeigen.
+
Koordinaten
+
Zeigt Koordinaten an den Rändern des Spielbretts für die Felder auf dem +Spielbrett. Die Konvention für die Koordinaten ist dieselbe wie im von Pentobi +benutzten Blokus-SGF-Dateiformat.
+
Varianten zeigen
+
Fügt einen Buchstaben an die Zugnummer auf dem Spielbrett an, wenn der Zug +Varianten besitzt. Wenn Züge mit einem Punkt statt einer Nummer markiert +werden, wird ein Kreis statt ein Punkt für Züge verwendet, die nicht in der +Hauptvariante sind.
+
Vollbild
+
Schaltet das Hauptfenster in den Vollbildmodus oder verlässt den +Vollbildmodus. Es ist systemabhängig, ob das Fenstermenü im Vollbildmodus +angezeigt wird. Um den Vollbildmodus ohne das Benutzen des Fenstermenüs zu +verlassen, drücken Sie die F11-Taste.
+
+

Computer

+
+
Computer-Farben
+
Wählt aus, welche Farben vom Computer gespielt werden.
+
Spielen
+
Lässt den Computer einen Zug für die gegenwärtige Farbe spielen. Dies kann +zum Ändern der Computer-Farbe benutzt werden oder um nach dem Navigieren im +Spielbaum mit dem Spielen fortzufahren. Wenn der Computer die gegenwärtige +Farbe nicht bereits spielte, wird er diese Farbe (und nur diese) im weiteren +Spielverlauf spielen.
+
Einzelnen Zug spielen
+
Lässt den Computer einen einzelnen Zug für die gegenwärtige Farbe spielen +ohne die vom Computer gespielten Farben zu ändern.
+
Stopp
+
Bricht die gegenwärtige Zuggenerierung ab. Sie können den Computer +weiterspielen lassen, indem Sie Spielen auswählen.
+
Spielstufe
+
Ändert die Spielstärke des Computers. Höhere Spielstufen sind stärker, +können aber die Bedenkzeiten des Computers auf langsamen Computern sehr +verlängern.
+
+

Extras

+
+
Wertung
+
Zeigt ein Dialogfenster mit der Wertung des Benutzers in der gegenwärtigen +Spielvariante.
+
Spiel analysieren
+
Führt eine Spielanalyse +durch.
+
+

Hilfe

+
+
Pentobi-Hilfe
+
Zeigt ein Fenster mit dem Pentobi-Benutzerhandbuch.
+
Über Pentobi
+
Zeigt eine Dialogfenster mit Informationen über diese Version von +Pentobi.
+
+

Zurück | Weiter

+ + diff --git a/src/pentobi/icons.qrc b/src/pentobi/icons.qrc new file mode 100644 index 0000000..735f5c8 --- /dev/null +++ b/src/pentobi/icons.qrc @@ -0,0 +1,15 @@ + + + + icons/pentobi-backward.svg + icons/pentobi-beginning.svg + icons/pentobi-computer-colors.svg + icons/pentobi-end.svg + icons/pentobi-forward.svg + icons/pentobi-newgame.svg + icons/pentobi-next-variation.svg + icons/pentobi-play.svg + icons/pentobi-previous-variation.svg + icons/pentobi-undo.svg + + diff --git a/src/pentobi/icons/pentobi-16.svg b/src/pentobi/icons/pentobi-16.svg new file mode 100644 index 0000000..9688842 --- /dev/null +++ b/src/pentobi/icons/pentobi-16.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi/icons/pentobi-32.svg b/src/pentobi/icons/pentobi-32.svg new file mode 100644 index 0000000..387a5b4 --- /dev/null +++ b/src/pentobi/icons/pentobi-32.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi/icons/pentobi-64.svg b/src/pentobi/icons/pentobi-64.svg new file mode 100644 index 0000000..4f371cd --- /dev/null +++ b/src/pentobi/icons/pentobi-64.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi/icons/pentobi-backward-16.svg b/src/pentobi/icons/pentobi-backward-16.svg new file mode 100644 index 0000000..42a4540 --- /dev/null +++ b/src/pentobi/icons/pentobi-backward-16.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/icons/pentobi-backward.svg b/src/pentobi/icons/pentobi-backward.svg new file mode 100644 index 0000000..7fbe3eb --- /dev/null +++ b/src/pentobi/icons/pentobi-backward.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/icons/pentobi-beginning-16.svg b/src/pentobi/icons/pentobi-beginning-16.svg new file mode 100644 index 0000000..68f4afb --- /dev/null +++ b/src/pentobi/icons/pentobi-beginning-16.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/icons/pentobi-beginning.svg b/src/pentobi/icons/pentobi-beginning.svg new file mode 100644 index 0000000..9e754ac --- /dev/null +++ b/src/pentobi/icons/pentobi-beginning.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/icons/pentobi-computer-colors-16.svg b/src/pentobi/icons/pentobi-computer-colors-16.svg new file mode 100644 index 0000000..aa6f6ac --- /dev/null +++ b/src/pentobi/icons/pentobi-computer-colors-16.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi/icons/pentobi-computer-colors.svg b/src/pentobi/icons/pentobi-computer-colors.svg new file mode 100644 index 0000000..c9870f0 --- /dev/null +++ b/src/pentobi/icons/pentobi-computer-colors.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi/icons/pentobi-end-16.svg b/src/pentobi/icons/pentobi-end-16.svg new file mode 100644 index 0000000..d249048 --- /dev/null +++ b/src/pentobi/icons/pentobi-end-16.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/icons/pentobi-end.svg b/src/pentobi/icons/pentobi-end.svg new file mode 100644 index 0000000..f657d97 --- /dev/null +++ b/src/pentobi/icons/pentobi-end.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/icons/pentobi-flip-horizontal.svg b/src/pentobi/icons/pentobi-flip-horizontal.svg new file mode 100644 index 0000000..193ba08 --- /dev/null +++ b/src/pentobi/icons/pentobi-flip-horizontal.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi/icons/pentobi-flip-vertical.svg b/src/pentobi/icons/pentobi-flip-vertical.svg new file mode 100644 index 0000000..1d266c3 --- /dev/null +++ b/src/pentobi/icons/pentobi-flip-vertical.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi/icons/pentobi-forward-16.svg b/src/pentobi/icons/pentobi-forward-16.svg new file mode 100644 index 0000000..ebce7fd --- /dev/null +++ b/src/pentobi/icons/pentobi-forward-16.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/icons/pentobi-forward.svg b/src/pentobi/icons/pentobi-forward.svg new file mode 100644 index 0000000..8957c14 --- /dev/null +++ b/src/pentobi/icons/pentobi-forward.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/icons/pentobi-newgame-16.svg b/src/pentobi/icons/pentobi-newgame-16.svg new file mode 100644 index 0000000..17df58f --- /dev/null +++ b/src/pentobi/icons/pentobi-newgame-16.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/pentobi/icons/pentobi-newgame.svg b/src/pentobi/icons/pentobi-newgame.svg new file mode 100644 index 0000000..1a2c884 --- /dev/null +++ b/src/pentobi/icons/pentobi-newgame.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/pentobi/icons/pentobi-next-piece.svg b/src/pentobi/icons/pentobi-next-piece.svg new file mode 100644 index 0000000..f3d844d --- /dev/null +++ b/src/pentobi/icons/pentobi-next-piece.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi/icons/pentobi-next-variation-16.svg b/src/pentobi/icons/pentobi-next-variation-16.svg new file mode 100644 index 0000000..72a9727 --- /dev/null +++ b/src/pentobi/icons/pentobi-next-variation-16.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/icons/pentobi-next-variation.svg b/src/pentobi/icons/pentobi-next-variation.svg new file mode 100644 index 0000000..cc39628 --- /dev/null +++ b/src/pentobi/icons/pentobi-next-variation.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/icons/pentobi-piece-clear.svg b/src/pentobi/icons/pentobi-piece-clear.svg new file mode 100644 index 0000000..f8cdbd4 --- /dev/null +++ b/src/pentobi/icons/pentobi-piece-clear.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi/icons/pentobi-play-16.svg b/src/pentobi/icons/pentobi-play-16.svg new file mode 100644 index 0000000..8d09c1c --- /dev/null +++ b/src/pentobi/icons/pentobi-play-16.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/pentobi/icons/pentobi-play.svg b/src/pentobi/icons/pentobi-play.svg new file mode 100644 index 0000000..d860369 --- /dev/null +++ b/src/pentobi/icons/pentobi-play.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/pentobi/icons/pentobi-previous-piece.svg b/src/pentobi/icons/pentobi-previous-piece.svg new file mode 100644 index 0000000..1180935 --- /dev/null +++ b/src/pentobi/icons/pentobi-previous-piece.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi/icons/pentobi-previous-variation-16.svg b/src/pentobi/icons/pentobi-previous-variation-16.svg new file mode 100644 index 0000000..f98e8a0 --- /dev/null +++ b/src/pentobi/icons/pentobi-previous-variation-16.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/icons/pentobi-previous-variation.svg b/src/pentobi/icons/pentobi-previous-variation.svg new file mode 100644 index 0000000..2d1d3f8 --- /dev/null +++ b/src/pentobi/icons/pentobi-previous-variation.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/icons/pentobi-rated-game-16.svg b/src/pentobi/icons/pentobi-rated-game-16.svg new file mode 100644 index 0000000..6bfe814 --- /dev/null +++ b/src/pentobi/icons/pentobi-rated-game-16.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/pentobi/icons/pentobi-rated-game.svg b/src/pentobi/icons/pentobi-rated-game.svg new file mode 100644 index 0000000..8a714cf --- /dev/null +++ b/src/pentobi/icons/pentobi-rated-game.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/pentobi/icons/pentobi-rotate-left.svg b/src/pentobi/icons/pentobi-rotate-left.svg new file mode 100644 index 0000000..8043152 --- /dev/null +++ b/src/pentobi/icons/pentobi-rotate-left.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi/icons/pentobi-rotate-right.svg b/src/pentobi/icons/pentobi-rotate-right.svg new file mode 100644 index 0000000..72aa153 --- /dev/null +++ b/src/pentobi/icons/pentobi-rotate-right.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi/icons/pentobi-undo-16.svg b/src/pentobi/icons/pentobi-undo-16.svg new file mode 100644 index 0000000..991c9d4 --- /dev/null +++ b/src/pentobi/icons/pentobi-undo-16.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/pentobi/icons/pentobi-undo.svg b/src/pentobi/icons/pentobi-undo.svg new file mode 100644 index 0000000..45bc84e --- /dev/null +++ b/src/pentobi/icons/pentobi-undo.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/pentobi/icons/pentobi.svg b/src/pentobi/icons/pentobi.svg new file mode 100644 index 0000000..fea7ae6 --- /dev/null +++ b/src/pentobi/icons/pentobi.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi/pentobi.conf.in b/src/pentobi/pentobi.conf.in new file mode 100644 index 0000000..e128c3b --- /dev/null +++ b/src/pentobi/pentobi.conf.in @@ -0,0 +1,6 @@ +# Config file to override installation settings such that the executable +# can be tested without installation +BooksDir=@CMAKE_SOURCE_DIR@/src/books +HelpDir=@CMAKE_SOURCE_DIR@/src/pentobi/help +TranslationsPentobiDir=@CMAKE_BINARY_DIR@/src/pentobi +TranslationsLibPentobiGuiDir=@CMAKE_BINARY_DIR@/src/libpentobi_gui diff --git a/src/pentobi/pentobi.ico b/src/pentobi/pentobi.ico new file mode 100644 index 0000000000000000000000000000000000000000..a947864b1f18db5a413571a35321c604f282edd7 GIT binary patch literal 8216 zcmeHM4OCNQ7=Fi)zmUxe!5?sx3F^t9@CQ*qM1>Ltig^?#9tjGQ2+}FwgaaO3iPICr zPmma*ktS#&*k(g3Sqf$XN^P?FlLXG<<3A)e~O;7_ndpahx>ka_kEw| z`+2_$00YbcHwIY2QcHlj2)nxzf0x<;>_YcA9P)1~fWYAZY&P5QoErn+rZd2Yp&|2z z+KjY8j}8qA3|N5oHaua0nq`TlF#wh>K>@SE*B7c4zM_abBW0IWyS^C(!9w;Br_ghu z7hhTyFe{;8b=pTeUk?2`Bm7st31f#}4%_#O=x4VN1xkIkf^$XI%$22-a={(@%fuB1gZG{<}Fo$$&Fqd4slOA8))1hW(6s9j} z`h;6vwraaa9zv0Gi_SYox$`8G7PEZz&8%4==JTZY?zOB`ygP$&?6r1r$Lb^a?Ru?R zzBWIz<%rC&PO7u(#{+YFyiD%#3A61w5u@NcrmE_iLtjGupzeCKYnV%9h>MnTLneW%;+uk1$eDO%3M z)j_B_(`WyE>LluqXb#5KL?_Su5)aL%r9>^!WQpK;S?UcGSEq-o4~Vd1d(?_9Z&52u z(|6`nYFr}Uudz6OtEj3%!0K(UnN>Z+ZBO>p=7!m-?*T$X^m(7ScfRFeJfrQT+w>wGgv9HY@WU0z}ZLqI?L9#hPiWBzP zw`P~R&qm0lB8=f)uLs4~BR@hBw6tB`uD`pZgY}s&P#3&-lnYYVFS@sU!mAyc@hRYJP2es-V zHW##M;}>$9SLww~?G0LBoX8XO7M< zS9Q64v7hG|Jvo(^l6j&;tvJ$-R)dI?%n8Z9jEhd#Z;poxON?lWZz`*O%d>pu+{u(Q zZL-MMw-aoYU#&j87)E1BZz!5>VX<$+l-i)iTkEz)UqDl@y&G$}a!TYdW9h~&{*IuK z<9kQsvc_x-L$6)^X~NyhO)|8a&6m7$I)m$c0ZlV?@pu*1gKdo~zlDJ^NSBv|=CZ6% z&}EU)QGMOf|<4njkq8 zFXx2e%ikmi?kX;XU&R>rZ&Cy+*Vj$hHa_KAal#8|&xD{kApvFnE7IK%Mw>CLKo`D4 zKiEo;s}Ah#8)3`q92h@d10~U(P?q2a7N3T|$*qgfR{jIH)UAc#^&f!em5%@#)!j15Z2ys3L#r&CZ%K8d)$YqY;%TYba^fG_o2(8k0t*{*QCO8P)&cQQJO4nx&~fJ=SUTKk!4zI-shyl-W@H)B2kAt(P=< zn7-x=(tt(}i=XaABU68RzR;}DXaIit^fjl50i&M<`dyJR9)neiL9*2!;;~oG+<(6 zab*<${fQC9l;I>q=0A9TB0u8zMObEI z4q$bL@8>=~m!UE}0#Z`$fNWQ~;oCX4`4hvpb7AXI@V}~sj1H{d^QFR4p~olP>i_W57kt-pJ*lG0RI60{9MAzk0I{n P^tFUHKet~%|M142QBsI4 literal 0 HcmV?d00001 diff --git a/src/pentobi/pentobi.rc b/src/pentobi/pentobi.rc new file mode 100644 index 0000000..3c6d0d5 --- /dev/null +++ b/src/pentobi/pentobi.rc @@ -0,0 +1 @@ + IDI_ICON1 ICON DISCARDABLE "pentobi.ico" diff --git a/src/pentobi/resources.qrc b/src/pentobi/resources.qrc new file mode 100644 index 0000000..0a8988a --- /dev/null +++ b/src/pentobi/resources.qrc @@ -0,0 +1,37 @@ + + + +icons/pentobi.png +icons/pentobi-16.png +icons/pentobi-32.png +icons/pentobi-backward.png +icons/pentobi-backward-16.png +icons/pentobi-beginning.png +icons/pentobi-beginning-16.png +icons/pentobi-computer-colors.png +icons/pentobi-computer-colors-16.png +icons/pentobi-end.png +icons/pentobi-end-16.png +icons/pentobi-flip-horizontal.png +icons/pentobi-flip-vertical.png +icons/pentobi-forward.png +icons/pentobi-forward-16.png +icons/pentobi-newgame.png +icons/pentobi-newgame-16.png +icons/pentobi-next-piece.png +icons/pentobi-next-variation.png +icons/pentobi-next-variation-16.png +icons/pentobi-piece-clear.png +icons/pentobi-play.png +icons/pentobi-play-16.png +icons/pentobi-previous-piece.png +icons/pentobi-previous-variation.png +icons/pentobi-previous-variation-16.png +icons/pentobi-rated-game.png +icons/pentobi-rated-game-16.png +icons/pentobi-rotate-left.png +icons/pentobi-rotate-right.png +icons/pentobi-undo.png +icons/pentobi-undo-16.png + + diff --git a/src/pentobi/resources_2x.qrc b/src/pentobi/resources_2x.qrc new file mode 100644 index 0000000..0af20b7 --- /dev/null +++ b/src/pentobi/resources_2x.qrc @@ -0,0 +1,37 @@ + + + +icons/pentobi@2x.png +icons/pentobi-16@2x.png +icons/pentobi-32@2x.png +icons/pentobi-backward@2x.png +icons/pentobi-backward-16@2x.png +icons/pentobi-beginning@2x.png +icons/pentobi-beginning-16@2x.png +icons/pentobi-computer-colors@2x.png +icons/pentobi-computer-colors-16@2x.png +icons/pentobi-end@2x.png +icons/pentobi-end-16@2x.png +icons/pentobi-flip-horizontal@2x.png +icons/pentobi-flip-vertical@2x.png +icons/pentobi-forward@2x.png +icons/pentobi-forward-16@2x.png +icons/pentobi-newgame@2x.png +icons/pentobi-newgame-16@2x.png +icons/pentobi-next-piece@2x.png +icons/pentobi-next-variation@2x.png +icons/pentobi-next-variation-16@2x.png +icons/pentobi-piece-clear@2x.png +icons/pentobi-play@2x.png +icons/pentobi-play-16@2x.png +icons/pentobi-previous-piece@2x.png +icons/pentobi-previous-variation@2x.png +icons/pentobi-previous-variation-16@2x.png +icons/pentobi-rated-game@2x.png +icons/pentobi-rated-game-16@2x.png +icons/pentobi-rotate-left@2x.png +icons/pentobi-rotate-right@2x.png +icons/pentobi-undo@2x.png +icons/pentobi-undo-16@2x.png + + diff --git a/src/pentobi/translations/pentobi.ts b/src/pentobi/translations/pentobi.ts new file mode 100644 index 0000000..60127d0 --- /dev/null +++ b/src/pentobi/translations/pentobi.ts @@ -0,0 +1,14 @@ + + + + + MainWindow + + %n move(s) + + %n move + %n moves + + + + diff --git a/src/pentobi/translations/pentobi_de.ts b/src/pentobi/translations/pentobi_de.ts new file mode 100644 index 0000000..029a4c8 --- /dev/null +++ b/src/pentobi/translations/pentobi_de.ts @@ -0,0 +1,1116 @@ + + + + + AnalyzeGameWidget + + Win + Gewinn + + + Loss + Verlust + + + Running game analysis... + Spiel wird analysiert ... + + + + AnalyzeGameWindow + + Game Analysis + Spielanalyse + + + + AnalyzeSpeedDialog + + Fast + Schnell + + + Normal + Normal + + + Slow + Langsam + + + Analysis speed: + Analysegeschwindigkeit: + + + + MainWindow + + About Pentobi + Über Pentobi + + + Next &Color + Nächste &Farbe + + + S&etup Mode + &Stellungsaufbau + + + Could not read file '%1' + Datei '%1' konnte nicht gelesen werden + + + The file is not a valid Blokus SGF file. + Die Datei ist keine gültige Blokus-SGF-Datei. + + + Truncate this subtree? + Diesen Teilbaum abschneiden? + + + This position and all following moves and variations will be removed from the game tree. + Diese Brettstellung und alle folgenden Züge und Varianten werden aus dem Spielbaum entfernt. + + + Truncate + Abschneiden + + + Truncate children? + Kindknoten abschneiden? + + + All following moves and variations will be removed from the game tree. + Alle folgenden Züge und Varianten werden aus dem Spielbaum entfernt. + + + Truncate Children + Kindknoten abschneiden + + + Could not delete %1 + %1 konnte nicht gelöscht werden + + + Rated game + Gewertetes Spiel + + + Continuing unfinished rated game. + Unbeendetes gewertetes Spiel wird fortgesetzt. + + + You play %1 in this game. + Sie spielen %1 in diesem Spiel. + + + &copy; 2011&ndash;%1 Markus Enzenberger + &copy; 2011&ndash;%1 Markus Enzenberger + + + Analyze Game + Spiel analysieren + + + &About Pentobi + Über &Pentobi + + + &Analyze Game... + Spiel &analysieren ... + + + B&ackward + &Zurück + + + Go one move backward + Einen Zug zurück gehen + + + Back to &Main Variation + Zurück zu Hau&ptvariante + + + &Bad + Schl&echt + + + &Beginning + &Anfang + + + Go to beginning of game + Zum Anfang des Spiels gehen + + + Beginning of Bran&ch + Anfang der Verz&weigung + + + Clear Piece + Spielstein löschen + + + &Computer Colors + &Computer-Farben + + + Set the colors played by the computer + Die vom Computer gespielten Farben festlegen + + + C&oordinates + K&oordinaten + + + &Delete All Variations + Alle &Varianten löschen + + + &Doubtful + &Zweifelhaft + + + &End + &Ende + + + Go to end of moves + Zum Ende der Züge gehen + + + &ASCII Art + &ASCII-Art + + + I&mage + &Grafik + + + &Find Move + Zug fin&den + + + Flip Horizontally + Waagrecht umdrehen + + + Flip Vertically + Senkrecht umdrehen + + + &Forward + &Vorwärts + + + Go one move forward + Einen Zug vorwärts gehen + + + &Fullscreen + Voll&bild + + + Ga&me Info + Spielinf&ormation + + + St&op + St&opp + + + &Duo + &Duo + + + &Good + &Gut + + + Game analysis is only possible in the main variation. + Spielanalyse ist nur in der Hauptvariante möglich. + + + Find Next &Comment + Nächsten &Kommentar finden + + + &Go to Move... + &Gehe zu Zug ... + + + Pentobi &Help + Pentobi-&Hilfe + + + I&nteresting + I&nteressant + + + &Keep Only Position + Nur &Brettstellung behalten + + + Keep Only &Subtree + Nur &Teilbaum behalten + + + Leave Fullscreen + Vollbild verlassen + + + M&ake Main Variation + Zu &Hauptvariante machen + + + Move Variation D&own + Variante nach &unten schieben + + + Move Variation &Up + Variante nach &oben schieben + + + &1 + &1 + + + &2 + &2 + + + &3 + &3 + + + &4 + &4 + + + &5 + &5 + + + &6 + &6 + + + &7 + &7 + + + &8 + &8 + + + &None + move numbers + &Keine + + + Next Piece + Nächster Spielstein + + + &Next Variation + &Nächste Variante + + + Go to next variation + Zur nächsten Variante gehen + + + &Rated Game + Ge&wertetes Spiel + + + &New + &Neu + + + Start a new game + Ein neues Spiel beginnen + + + N&one + move annotation + &Keine + + + &Open... + Öffn&en ... + + + &Play + &Spielen + + + Play &Single Move + &Einzelnen Zug spielen + + + Previous Piece + Vorheriger Spielstein + + + &Previous Variation + Vor&herige Variante + + + Go to previous variation + Zur vorherigen Variante gehen + + + Rotate Anticlockwise + Gegen den Uhrzeigersinn drehen + + + Rotate Clockwise + Im Uhrzeigersinn drehen + + + &Quit + &Beenden + + + &Save + &Speichern + + + Save &As... + Speichern &unter ... + + + &Comment + &Kommentar + + + &Rating + &Wertung + + + &No Text + &Kein Text + + + Text &Beside Icons + Text n&eben Symbolen + + + Text Bel&ow Icons + Text &unter Symbolen + + + &Text Only + &Nur Text + + + &System Default + &Systemvorgabe + + + &Truncate + &Abschneiden + + + Truncate C&hildren + &Kindknoten abschneiden + + + Show &Variations + &Varianten zeigen + + + &Undo Move + Zug rück&gängig + + + V&ery Bad + Seh&r schlecht + + + &Very Good + &Sehr gut + + + &Edit + &Bearbeiten + + + &Move Annotation + &Zugkommentierung + + + &View + &Ansicht + + + Toolbar T&ext + Werkzeugleistent&ext + + + &Computer + &Computer + + + &Tools + E&xtras + + + &Help + &Hilfe + + + Delete all variations? + Alle Varianten löschen? + + + All variations but the main variation will be removed from the game tree. + Alle Varianten außer der Hauptvariante werden aus dem Spielbaum entfernt. + + + Delete Variations + Varianten löschen + + + &Toolbar + &Werkzeugleiste + + + Text files (*.txt);;All files (*) + Textdateien (*.txt);;Alle Dateien (*) + + + No comment found + Kein Kommentar gefunden + + + Blokus games (*.blksgf);;All files (*) + Blokus-Partien (*.blksgf);;Alle Dateien (*) + + + Move number: + Zugnummer: + + + Go to Move + Gehe zu Zug + + + Keep only position? + Nur Brettstellung behalten? + + + All previous and following moves and variations will be removed from the game tree. + Alle vorhergehenden und nachfolgenden Züge und Varianten werden aus dem Spielbaum entfernt. + + + Keep only subtree? + Nur Teilbaum behalten? + + + All previous moves and variations will be removed from the game tree. + Alle vorhergehenden Züge und Varianten werden aus dem Spielbaum entfernt. + + + Start rated game? + Gewertetes Spiel beginnen? + + + In this game, you play %1 against Pentobi level&nbsp;%2. + In diesem Spiel spielen Sie %1 gegen Pentobi Spielstufe&nbsp;%2. + + + &Start Game + &Spiel beginnen + + + Pentobi %1 (level %2) + The first argument is the version of Pentobi + Pentobi %1 (Stufe %2) + + + Human + Mensch + + + Open + Öffnen + + + The file could not be saved. + Die Datei konnte nicht gespeichert werden. + + + %1: %2 + Error message if file cannot be saved. %1 is replaced by the file name, %2 by the error message of the operating system. + %1: %2 + + + Untitled Game.blksgf + Unbenanntes Spiel.blksgf + + + Untitled Game %1.blksgf + Unbenanntes Spiel %1.blksgf + + + Save + Speichern + + + Pentobi + Pentobi + + + Version %1 + Version %1 + + + Computer opponent for the board game Blokus. + Computer-Gegner für das Brettspiel Blokus. + + + The file has been modified. + Die Datei wurde geändert. + + + Do you want to save your changes? + Änderungen speichern? + + + &Don't Save + &Nicht speichern + + + The current game is not finished. + Das gegenwärtige Spiel ist nicht beendet. + + + Do you want to abort the game? + Möchten Sie das Spiel verwerfen? + + + &Abort Game + Spiel &verwerfen + + + &9 + &9 + + + &All with Number + &Alle mit Nummer + + + Last with &Dot + Letzter mit &Punkt + + + &Last with Number + Letzter mit &Nummer + + + Start a rated game + Ein gewertetes Spiel beginnen + + + Classic (&3 Players) + Klassisch (&3 Spieler) + + + &Game + &Spiel + + + Game &Variant + Spiel&variante + + + Open R&ecent + &Zuletzt benutzte Dateien + + + E&xport + E&xportieren + + + G&o + &Gehe zu + + + &Move Marking + &Zugmarkierung + + + Export Image + Grafik exportieren + + + Image size: + Bildgröße: + + + The end of the tree was reached. + Das Ende des Spielbaums wurde erreicht. + + + Continue the search from the start of the tree? + Die Suche vom Beginn des Spielbaums fortsetzen? + + + Continue From Start + Suche vom Beginn fortsetzen + + + Show &Rating + &Wertung zeigen + + + Pentobi Help + Pentobi-Hilfe + + + Keep Only Position + Nur Brettstellung behalten + + + Keep Only Subtree + Nur Teilbaum behalten + + + [*]%1 + [*]%1 + + + Setup mode cannot be used if moves have been played. + Stellungsaufbau-Modus kann nicht benutzt werden, wenn Züge gespielt wurden. + + + Setup mode + Stellungsaufbau + + + The game ends in a tie. + Das Spiel endet in einem Unentschieden. + + + The game ends in a tie between all colors. + Das Spiel endet in einem Unentschieden zwischen allen Farben. + + + The game ends in a tie between Blue, Yellow and Red. + Das Spiel endet in einem Unentschieden zwischen Blau, Gelb und Rot. + + + The game ends in a tie between Blue, Yellow and Green. + Das Spiel endet in einem Unentschieden zwischen Blau, Gelb und Grün. + + + The game ends in a tie between Blue, Red and Green. + Das Spiel endet in einem Unentschieden zwischen Blau, Rot und Grün. + + + The game ends in a tie between Yellow, Red and Green. + Das Spiel endet in einem Unentschieden zwischen Gelb, Rot und Grün. + + + The game ends in a tie between Blue and Yellow. + Das Spiel endet in einem Unentschieden zwischen Blau und Gelb. + + + The game ends in a tie between Blue and Red. + Das Spiel endet in einem Unentschieden zwischen Blau und Rot. + + + The game ends in a tie between Blue and Green. + Das Spiel endet in einem Unentschieden zwischen Blau und Grün. + + + The game ends in a tie between Yellow and Red. + Das Spiel endet in einem Unentschieden zwischen Gelb und Rot. + + + The game ends in a tie between Yellow and Green. + Das Spiel endet in einem Unentschieden zwischen Gelb und Grün. + + + The game ends in a tie between Red and Green. + Das Spiel endet in einem Unentschieden zwischen Rot und Grün. + + + Blue wins. + Blau gewinnt. + + + Yellow wins. + Gelb gewinnt. + + + Red wins. + Rot gewinnt. + + + Green wins. + Grün gewinnt. + + + Your rating has increased from %1 to %2. + Ihre Wertung hat sich von %1 auf %2 erhöht. + + + Your rating stays at %1. + Ihre Wertung bleibt auf %1. + + + Your rating has decreased from %1 to %2. + Ihre Wertung hat sich von %1 auf %2 erniedrigt. + + + Error in file '%1' + Fehler in Datei '%1' + + + Move %1 + Zug %1 + + + %n move(s) + + %n Zug + %n Züge + + + + Move %1 of %2 + Zug %1 von %2 + + + Move %1 of %2 in variation %3 + Zug %1 von %2 in Variante %3 + + + Make the computer continue to play the current color + Den Computer die gegenwärtige Farbe weiterspielen lassen + + + Make the computer play the current color + Den Computer die gegenwärtige Farbe spielen lassen + + + Classic (&4 Players) + Klassisch (&4 Spieler) + + + J&unior + J&unior + + + Classic (&2 Players) + Klassisch (&2 Spieler) + + + Nexos (&2 Players) + Nexos (&2 Spieler) + + + Nexos (&4 Players) + Nexos (&4 Spieler) + + + Trigon (&2 Players) + Trigon (&2 Spieler) + + + Trigon (&3 Players) + Trigon (&3 Spieler) + + + Trigon (&4 Players) + Trigon (&4 Spieler) + + + &Classic + &Klassisch + + + &Trigon + &Trigon + + + &Nexos + &Nexos + + + Blue wins with 1 point. + Blau gewinnt mit 1 Punkt. + + + Blue wins with %1 points. + Blau gewinnt mit %1 Punkten. + + + Green wins with 1 point. + Grün gewinnt mit 1 Punkt. + + + Green wins with %1 points. + Grün gewinnt mit %1 Punkten. + + + Blue/Red wins with 1 point. + Blau/Rot gewinnt mit 1 Punkt. + + + Blue/Red wins with %1 points. + Blau/Rot gewinnt mit %1 Punkten. + + + Yellow/Green wins with 1 point. + Gelb/Grün gewinnt mit 1 Punkt. + + + Yellow/Green wins with %1 points. + Gelb/Grün gewinnt mit %1 Punkten. + + + Callisto (&2 Players) + Callisto (&2 Spieler) + + + Callisto (&3 Players) + Callisto (&3 Spieler) + + + Callisto (&4 Players) + Callisto (&4 Spieler) + + + C&allisto + &Callisto + + + Green wins (tie resolved). + Grün gewinnt (Unentschieden aufgelöst). + + + Yellow/Green wins (tie resolved). + Gelb/Grün gewinnt (Unentschieden aufgelöst). + + + Red wins (tie resolved). + Rot gewinnt (Unentschieden aufgelöst). + + + Yellow wins (tie resolved). + Gelb gewinnt (Unentschieden aufgelöst). + + + &Level (Classic, 4 Players) + Spielst&ufe (Klassisch, 4 Spieler) + + + &Level (Classic, 2 Players) + Spielst&ufe (Klassisch, 2 Spieler) + + + &Level (Classic, 3 Players) + Spielst&ufe (Klassisch, 3 Spieler) + + + &Level (Duo) + Spielst&ufe (Duo) + + + &Level (Trigon, 4 Players) + Spielst&ufe (Trigon, 4 Spieler) + + + &Level (Trigon, 2 Players) + Spielst&ufe (Trigon, 2 Spieler) + + + &Level (Trigon, 3 Players) + Spielst&ufe (Trigon, 3 Spieler) + + + &Level (Junior) + Spielst&ufe (Junior) + + + &Level (Nexos, 4 Players) + Spielst&ufe (Nexos, 4 Spieler) + + + &Level (Nexos, 2 Players) + Spielst&ufe (Nexos, 2 Spieler) + + + &Level (Callisto, 4 Players) + Spielst&ufe (Callisto, 4 Spieler) + + + &Level (Callisto, 2 Players) + Spielst&ufe (Callisto, 2 Spieler) + + + &Level (Callisto, 3 Players) + Spielst&ufe (Callisto, 3 Spieler) + + + Computer is thinking... (up to %1 seconds remaining) + Computer denkt ... (maximal %1 Sekunden verbleibend) + + + Computer is thinking... (up to %1 minutes remaining) + Computer denkt ... (maximal %1 Minuten verbleibend) + + + Computer is thinking... + Computer denkt ... + + + + RatedGamesList + + Game + Spiel + + + Your Color + Ihre Farbe + + + Level + Stufe + + + Result + Ergebnis + + + Date + Datum + + + Win + Gewinn + + + Tie + Unentschieden + + + Loss + Verlust + + + + RatingDialog + + Rating + Wertung + + + Your rating: + Ihre Wertung: + + + Game variant: + Spielvariante: + + + Number rated games: + Anzahl gewertete Spiele: + + + Best previous rating: + Beste frühere Wertung: + + + Recent games: + Zuletzt gespielte Spiele: + + + &Clear + &Löschen + + + Clear rating and delete rating history? + Wertung und Wertungsentwicklung löschen? + + + Clear rating + Wertung löschen + + + Classic (4 players) + Klassisch (4 Spieler) + + + Classic (2 players) + Klassisch (2 Spieler) + + + Classic (3 players) + Klassisch (3 Spieler) + + + Duo + Duo + + + Trigon (4 players) + Trigon (4 Spieler) + + + Trigon (2 players) + Trigon (2 Spieler) + + + Trigon (3 players) + Trigon (3 Spieler) + + + Junior + Junior + + + Recent development: + Aktuelle Entwicklung: + + + Nexos (4 players) + Nexos (4 Spieler) + + + Nexos (2 players) + Nexos (2 Spieler) + + + Callisto (4 players) + Callisto (4 Spieler) + + + Callisto (2 players) + Callisto (2 Spieler) + + + Callisto (3 players) + Callisto (3 Spieler) + + + + main + + Not enough memory. + Nicht genügend Speicher. + + + Pentobi + Pentobi + + + diff --git a/src/pentobi_gtp/CMakeLists.txt b/src/pentobi_gtp/CMakeLists.txt new file mode 100644 index 0000000..d42d74c --- /dev/null +++ b/src/pentobi_gtp/CMakeLists.txt @@ -0,0 +1,24 @@ +add_executable(pentobi-gtp + Engine.h + Engine.cpp + Main.cpp +) + +target_link_libraries(pentobi-gtp + pentobi_mcts + pentobi_base + boardgame_base + boardgame_sgf + boardgame_util + boardgame_sys + boardgame_gtp + ) + +if(CMAKE_THREAD_LIBS_INIT) + target_link_libraries(pentobi-gtp ${CMAKE_THREAD_LIBS_INIT}) +endif() + +if(MINGW AND (CMAKE_SIZEOF_VOID_P EQUAL "4")) + set_target_properties(pentobi-gtp PROPERTIES LINK_FLAGS -Wl,--large-address-aware) +endif() + diff --git a/src/pentobi_gtp/Engine.cpp b/src/pentobi_gtp/Engine.cpp new file mode 100644 index 0000000..07734c3 --- /dev/null +++ b/src/pentobi_gtp/Engine.cpp @@ -0,0 +1,183 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_gtp/Engine.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Engine.h" + +#include +#include "libpentobi_mcts/Util.h" + +namespace pentobi_gtp { + +using libboardgame_gtp::Failure; +using libpentobi_mcts::Float; + +//----------------------------------------------------------------------------- + +Engine::Engine(Variant variant, unsigned level, bool use_book, + const string& books_dir, unsigned nu_threads) + : libpentobi_base::Engine(variant) +{ + create_player(variant, level, books_dir, nu_threads); + get_mcts_player().set_use_book(use_book); + add("get_value", &Engine::cmd_get_value); + add("name", &Engine::cmd_name); + add("param", &Engine::cmd_param); + add("move_values", &Engine::cmd_move_values); + add("save_tree", &Engine::cmd_save_tree); + add("version", &Engine::cmd_version); +} + +Engine::~Engine() = default; + +void Engine::cmd_get_value(Response& response) +{ + response << get_search().get_tree().get_root().get_value(); +} + +void Engine::cmd_move_values(Response& response) +{ + auto& search = get_search(); + auto& tree = search.get_tree(); + auto& bd = get_board(); + vector children; + for (auto& i : tree.get_root_children()) + children.push_back(&i); + sort(children.begin(), children.end(), libpentobi_mcts::util::compare_node); + response << fixed; + for (auto node : children) + response << setprecision(0) << node->get_visit_count() << ' ' + << setprecision(1) << node->get_value_count() << ' ' + << setprecision(3) << node->get_value() << ' ' + << bd.to_string(node->get_move(), true) << '\n'; +} + +void Engine::cmd_name(Response& response) +{ + response.set("Pentobi"); +} + +void Engine::cmd_save_tree(const Arguments& args) +{ + auto& search = get_search(); + if (! search.get_last_history().is_valid()) + throw Failure("no search tree"); + ofstream out(args.get()); + libpentobi_mcts::util::dump_tree(out, search); +} + +void Engine::cmd_param(const Arguments& args, Response& response) +{ + auto& p = get_mcts_player(); + auto& s = get_search(); + if (args.get_size() == 0) + response + << "avoid_symmetric_draw " << s.get_avoid_symmetric_draw() << '\n' + << "auto_param " << s.get_auto_param() << '\n' + << "exploration_constant " << s.get_exploration_constant() << '\n' + << "expand_threshold " << s.get_expand_threshold() << '\n' + << "expand_threshold_inc " << s.get_expand_threshold_inc() << '\n' + << "fixed_simulations " << p.get_fixed_simulations() << '\n' + << "rave_child_max " << s.get_rave_child_max() << '\n' + << "rave_parent_max " << s.get_rave_parent_max() << '\n' + << "rave_weight " << s.get_rave_weight() << '\n' + << "reuse_subtree " << s.get_reuse_subtree() << '\n' + << "use_book " << p.get_use_book() << '\n'; + else + { + args.check_size(2); + string name = args.get(0); + if (name == "avoid_symmetric_draw") + s.set_avoid_symmetric_draw(args.parse(1)); + else if (name == "auto_param") + s.set_auto_param(args.parse(1)); + else if (name == "exploration_constant") + s.set_exploration_constant(args.parse(1)); + else if (name == "expand_threshold") + s.set_expand_threshold(args.parse(1)); + else if (name == "expand_threshold_inc") + s.set_expand_threshold_inc(args.parse(1)); + else if (name == "fixed_simulations") + p.set_fixed_simulations(args.parse(1)); + else if (name == "rave_child_max") + s.set_rave_child_max(args.parse(1)); + else if (name == "rave_parent_max") + s.set_rave_parent_max(args.parse(1)); + else if (name == "rave_weight") + s.set_rave_weight(args.parse(1)); + else if (name == "reuse_subtree") + s.set_reuse_subtree(args.parse(1)); + else if (name == "use_book") + p.set_use_book(args.parse(1)); + else + { + ostringstream msg; + msg << "unknown parameter '" << name << "'"; + throw Failure(msg.str()); + } + } +} + +void Engine::cmd_version(Response& response) +{ + string version; +#ifdef VERSION + version = VERSION; +#endif + if (version.empty()) + version = "UNKNOWN"; + // By convention, the version string of unreleased versions contains the + // string UNKNOWN (appended to the last released version). In this case, or + // if VERSION was undefined, we append the build date. + if (version.find("UNKNOWN") != string::npos) + { + version.append(" ("); + version.append(__DATE__); + version.append(")"); + } +#if LIBBOARDGAME_DEBUG + version.append(" (dbg)"); +#endif + response.set(version); +} + +void Engine::create_player(Variant variant, unsigned level, + const string& books_dir, unsigned nu_threads) +{ + auto max_level = level; + m_player.reset(new Player(variant, max_level, books_dir, nu_threads)); + get_mcts_player().set_level(level); + set_player(*m_player); +} + +Player& Engine::get_mcts_player() +{ + try + { + return dynamic_cast(*m_player); + } + catch (const bad_cast&) + { + throw Failure("current player is not mcts player"); + } +} + +Search& Engine::get_search() +{ + return get_mcts_player().get_search(); +} + +void Engine::use_cpu_time(bool enable) +{ + get_mcts_player().use_cpu_time(enable); +} + +//----------------------------------------------------------------------------- + +} // namespace pentobi_gtp diff --git a/src/pentobi_gtp/Engine.h b/src/pentobi_gtp/Engine.h new file mode 100644 index 0000000..76418b3 --- /dev/null +++ b/src/pentobi_gtp/Engine.h @@ -0,0 +1,60 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_gtp/Engine.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_GTP_ENGINE_H +#define PENTOBI_GTP_ENGINE_H + +#include "libpentobi_base/Engine.h" +#include "libpentobi_mcts/Player.h" + +namespace pentobi_gtp { + +using namespace std; +using libboardgame_gtp::Arguments; +using libboardgame_gtp::Response; +using libpentobi_base::PlayerBase; +using libpentobi_base::Variant; +using libpentobi_mcts::Player; +using libpentobi_mcts::Search; + +//----------------------------------------------------------------------------- + +class Engine + : public libpentobi_base::Engine +{ +public: + Engine(Variant variant, unsigned level = 5, + bool use_book = true, const string& books_dir = "", + unsigned nu_threads = 0); + + ~Engine(); + + void cmd_param(const Arguments&, Response&); + void cmd_get_value(Response&); + void cmd_move_values(Response&); + void cmd_name(Response&); + void cmd_save_tree(const Arguments&); + void cmd_version(Response&); + + Player& get_mcts_player(); + + /** @see Player::use_cpu_time() */ + void use_cpu_time(bool enable); + +private: + unique_ptr m_player; + + void create_player(Variant variant, unsigned level, + const string& books_dir, unsigned nu_threads); + + Search& get_search(); +}; + +//----------------------------------------------------------------------------- + +} // namespace pentobi_gtp + +#endif // PENTOBI_GTP_ENGINE_H diff --git a/src/pentobi_gtp/Main.cpp b/src/pentobi_gtp/Main.cpp new file mode 100644 index 0000000..3fa9087 --- /dev/null +++ b/src/pentobi_gtp/Main.cpp @@ -0,0 +1,170 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_gtp/Main.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include "Engine.h" +#include "libboardgame_util/Log.h" +#include "libboardgame_util/Options.h" +#include "libboardgame_util/RandomGenerator.h" + +using namespace std; +using libboardgame_gtp::Failure; +using libboardgame_util::Options; +using libboardgame_util::RandomGenerator; +using libpentobi_base::parse_variant_id; +using libpentobi_base::Board; +using libpentobi_base::Variant; +using libpentobi_mcts::Player; + +//----------------------------------------------------------------------------- + +namespace { + +string get_application_dir_path(int argc, char** argv) +{ + if (argc == 0 || ! argv || ! argv[0]) + return ""; + string application_path(argv[0]); +#ifdef _WIN32 + auto pos = application_path.find_last_of("/\\"); +#else + auto pos = application_path.find_last_of("/"); +#endif + if (pos == string::npos) + return ""; + return application_path.substr(0, pos); +} + +} // namespace + +//----------------------------------------------------------------------------- + +int main(int argc, char** argv) +{ + string application_dir_path = get_application_dir_path(argc, argv); + try + { + vector specs = { + "book:", + "config|c:", + "color", + "cputime", + "game|g:", + "help|h", + "level|l:", + "nobook", + "noresign", + "quiet|q", + "seed|r:", + "showboard", + "threads:", + "version|v" + }; + Options opt(argc, argv, specs); + if (opt.contains("help")) + { + cout << + "Usage: pentobi_gtp [options] [input files]\n" + "--book load an external book file\n" + "--config,-c set GTP config file\n" + "--color colorize text output of boards\n" + "--cputime use CPU time\n" + "--game,-g game variant (classic, classic_2, classic_3,\n" + " duo, trigon, trigon_2, trigon_3, junior)\n" + "--help,-h print help message and exit\n" + "--level,-l set playing strength level\n" + "--seed,-r set random seed\n" + "--showboard automatically write board to stderr after\n" + " changes\n" + "--nobook disable opening book\n" + "--noresign disable resign\n" + "--quiet,-q do not print logging messages\n" + "--threads number of threads in the search\n" + "--version,-v print version and exit\n"; + return 0; + } + if (opt.contains("version")) + { +#ifdef VERSION + cout << "Pentobi " << VERSION << '\n'; +#else + cout << "Pentobi UNKNONW"; +#endif + return 0; + } + unsigned threads = 1; + if (opt.contains("threads")) + { + threads = opt.get("threads"); + if (threads == 0) + throw runtime_error("Number of threads must be greater zero."); + } + Board::color_output = opt.contains("color"); + if (opt.contains("quiet")) + libboardgame_util::disable_logging(); + if (opt.contains("seed")) + RandomGenerator::set_global_seed( + opt.get("seed")); + string variant_string = opt.get("game", "classic"); + Variant variant; + if (! parse_variant_id(variant_string, variant)) + throw runtime_error("invalid game variant " + variant_string); + auto level = opt.get("level", 4); + if (level < 1 || level > Player::max_supported_level) + throw runtime_error("invalid level"); + auto use_book = (! opt.contains("nobook")); + string books_dir = application_dir_path; + pentobi_gtp::Engine engine(variant, level, use_book, books_dir, + threads); + engine.set_resign(! opt.contains("noresign")); + if (opt.contains("showboard")) + engine.set_show_board(true); + if (opt.contains("cputime")) + engine.use_cpu_time(true); + string book_file = opt.get("book", ""); + if (! book_file.empty()) + { + ifstream in(book_file); + engine.get_mcts_player().load_book(in); + } + string config_file = opt.get("config", ""); + if (! config_file.empty()) + { + ifstream in(config_file); + if (! in) + throw runtime_error("Error opening " + config_file); + engine.exec(in, true, libboardgame_util::get_log_stream()); + } + auto& args = opt.get_args(); + if (! args.empty()) + for (auto& file : args) + { + ifstream in(file); + if (! in) + throw runtime_error("Error opening " + file); + engine.exec_main_loop(in, cout); + } + else + engine.exec_main_loop(cin, cout); + return 0; + } + catch (const Failure& e) + { + LIBBOARDGAME_LOG("Error: command in config file failed: ", e.what()); + return 1; + } + catch (const exception& e) + { + LIBBOARDGAME_LOG("Error: ", e.what()); + return 1; + } +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi_kde_thumbnailer/CMakeLists.txt b/src/pentobi_kde_thumbnailer/CMakeLists.txt new file mode 100644 index 0000000..bc3002f --- /dev/null +++ b/src/pentobi_kde_thumbnailer/CMakeLists.txt @@ -0,0 +1,23 @@ +find_package(ECM REQUIRED NO_MODULE) +set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) + +include(KDEInstallDirs) +include(KDECompilerSettings) +include(KDECMakeSettings) + +find_package(KF5 REQUIRED COMPONENTS KIO) + +include_directories(${CMAKE_SOURCE_DIR}/src) + +add_library(pentobi-thumbnail MODULE + PentobiThumbCreator.h + PentobiThumbCreator.cpp +) + +target_link_libraries(pentobi-thumbnail + pentobi_kde_thumbnailer + KF5::KIOWidgets +) + +install(TARGETS pentobi-thumbnail DESTINATION ${PLUGIN_INSTALL_DIR}) +install(FILES pentobi-thumbnail.desktop DESTINATION ${SERVICES_INSTALL_DIR}) diff --git a/src/pentobi_kde_thumbnailer/PentobiThumbCreator.cpp b/src/pentobi_kde_thumbnailer/PentobiThumbCreator.cpp new file mode 100644 index 0000000..ce003d6 --- /dev/null +++ b/src/pentobi_kde_thumbnailer/PentobiThumbCreator.cpp @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_kde_thumbnailer/PentobiThumbCreator.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "PentobiThumbCreator.h" + +#include +#include "libpentobi_thumbnail/CreateThumbnail.h" + +//----------------------------------------------------------------------------- + +extern "C" { + +Q_DECL_EXPORT ThumbCreator* new_creator() { return new PentobiThumbCreator; } + +} + +//----------------------------------------------------------------------------- + +PentobiThumbCreator::~PentobiThumbCreator() = default; + +bool PentobiThumbCreator::create(const QString& path, int width, int height, + QImage& image) +{ + image = QImage(width, height, QImage::Format_ARGB32); + if (image.isNull()) + return false; + image.fill(Qt::transparent); + return createThumbnail(path, width, height, image); +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi_kde_thumbnailer/PentobiThumbCreator.h b/src/pentobi_kde_thumbnailer/PentobiThumbCreator.h new file mode 100644 index 0000000..a28b14d --- /dev/null +++ b/src/pentobi_kde_thumbnailer/PentobiThumbCreator.h @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_kde_thumbnailer/PentobiThumbCreator.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_KDE_THUMBNAILER_PENTOBI_THUMB_CREATOR_H +#define PENTOBI_KDE_THUMBNAILER_PENTOBI_THUMB_CREATOR_H + +#include +#include + +//----------------------------------------------------------------------------- + +class PentobiThumbCreator + : public QObject, + public ThumbCreator +{ + Q_OBJECT + +public: + virtual ~PentobiThumbCreator(); + + bool create(const QString& path, int width, int height, + QImage& image) override; +}; + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_KDE_THUMBNAILER_PENTOBI_THUMB_CREATOR_H diff --git a/src/pentobi_kde_thumbnailer/pentobi-thumbnail.desktop b/src/pentobi_kde_thumbnailer/pentobi-thumbnail.desktop new file mode 100644 index 0000000..1acd6dc --- /dev/null +++ b/src/pentobi_kde_thumbnailer/pentobi-thumbnail.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Type=Service +Name=Blokus games +Name[de]=Blokus-Partien +ServiceTypes=ThumbCreator +MimeType=application/x-blokus-sgf; +X-KDE-Library=pentobi-thumbnail diff --git a/src/pentobi_qml/.gitignore b/src/pentobi_qml/.gitignore new file mode 100644 index 0000000..9bfc6e8 --- /dev/null +++ b/src/pentobi_qml/.gitignore @@ -0,0 +1 @@ +Pentobi.pro.user diff --git a/src/pentobi_qml/CMakeLists.txt b/src/pentobi_qml/CMakeLists.txt new file mode 100644 index 0000000..36db59d --- /dev/null +++ b/src/pentobi_qml/CMakeLists.txt @@ -0,0 +1,41 @@ +set(CMAKE_AUTOMOC TRUE) + +include_directories(${CMAKE_SOURCE_DIR}/src) + +set(pentobi_qml_SRCS + GameModel.h + GameModel.cpp + Main.cpp + PieceModel.h + PieceModel.cpp + PlayerModel.h + PlayerModel.cpp +) + +qt5_add_resources(pentobi_qml_RC_SRCS + resources.qrc + qml/themes/theme_light.qrc + qml/themes/theme_shared.qrc + ../books/pentobi_books.qrc + ../pentobi/icons.qrc +) + +add_executable(pentobi_qml WIN32 + ${pentobi_qml_SRCS} + ${pentobi_qml_RC_SRCS} +) + +target_link_libraries(pentobi_qml + pentobi_mcts + pentobi_base + boardgame_base + boardgame_sgf + boardgame_util + boardgame_sys +) + +qt5_use_modules(pentobi_qml Concurrent Gui Qml Svg) + +if(CMAKE_THREAD_LIBS_INIT) + target_link_libraries(pentobi_qml ${CMAKE_THREAD_LIBS_INIT}) +endif() diff --git a/src/pentobi_qml/GameModel.cpp b/src/pentobi_qml/GameModel.cpp new file mode 100644 index 0000000..28cf24b --- /dev/null +++ b/src/pentobi_qml/GameModel.cpp @@ -0,0 +1,807 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_qml/GameModel.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "GameModel.h" + +#include +#include +#include +#include +#include +#include "libboardgame_sgf/SgfUtil.h" +#include "libboardgame_sgf/TreeReader.h" +#include "libpentobi_base/PentobiTreeWriter.h" +#include "libpentobi_base/TreeUtil.h" + +using namespace std; +using libboardgame_sgf::InvalidTree; +using libboardgame_sgf::TreeReader; +using libboardgame_sgf::util::back_to_main_variation; +using libboardgame_sgf::util::get_last_node; +using libboardgame_sgf::util::is_main_variation; +using libpentobi_base::get_piece_set; +using libpentobi_base::to_string_id; +using libpentobi_base::BoardType; +using libpentobi_base::Color; +using libpentobi_base::ColorMap; +using libpentobi_base::ColorMove; +using libpentobi_base::CoordPoint; +using libpentobi_base::MovePoints; +using libpentobi_base::PentobiTree; +using libpentobi_base::PentobiTreeWriter; +using libpentobi_base::Piece; +using libpentobi_base::PieceInfo; +using libpentobi_base::PiecePoints; +using libpentobi_base::PieceSet; +using libpentobi_base::Point; +using libpentobi_base::tree_util::get_position_info; + +//----------------------------------------------------------------------------- + +namespace { + +// Game coordinates are fractional because they refer to the center of a piece. +// This function is used to compare game coordinates of moves with the same +// piece, so we could even compare the rounded values (?), but comparing +// against epsilon is also safe. +bool compareGameCoord(const QPointF& p1, const QPointF& p2) +{ + return (p1 - p2).manhattanLength() < 0.01f; +} + +bool compareTransform(const PieceInfo& pieceInfo, const Transform* t1, + const Transform* t2) +{ + return pieceInfo.get_equivalent_transform(t1) == + pieceInfo.get_equivalent_transform(t2); +} + +QPointF getGameCoord(const Board& bd, Move mv) +{ + auto& geo = bd.get_geometry(); + PiecePoints movePoints; + for (Point p : bd.get_move_points(mv)) + movePoints.push_back(CoordPoint(geo.get_x(p), geo.get_y(p))); + return PieceModel::findCenter(bd, movePoints, false); +} + +} //namespace + +//----------------------------------------------------------------------------- + +GameModel::GameModel(QObject* parent) + : QObject(parent), + m_game(getInitialGameVariant()), + m_gameVariant(to_string_id(m_game.get_variant())), + m_nuColors(getBoard().get_nu_colors()) +{ + createPieceModels(); + updateProperties(); +} + +void GameModel::autoSave() +{ + // Don't autosave if game was not modified because it could have been + // loaded from a file, but autosave if not modified and empty to ensure + // that we start with the same game variant next time. + if (! m_game.is_modified() + && ! libboardgame_sgf::util::is_empty(m_game.get_tree())) + return; + ostringstream s; + PentobiTreeWriter writer(s, m_game.get_tree()); + writer.set_indent(-1); + writer.write(); + QSettings settings; + settings.setValue("variant", to_string_id(m_game.get_variant())); + settings.setValue("autosave", s.str().c_str()); +} + +void GameModel::backToMainVar() +{ + gotoNode(back_to_main_variation(m_game.get_current())); +} + +void GameModel::createPieceModels() +{ + createPieceModels(Color(0), m_pieceModels0); + createPieceModels(Color(1), m_pieceModels1); + if (m_nuColors > 2) + createPieceModels(Color(2), m_pieceModels2); + else + m_pieceModels2.clear(); + if (m_nuColors > 3) + createPieceModels(Color(3), m_pieceModels3); + else + m_pieceModels3.clear(); +} + +void GameModel::createPieceModels(Color c, QList& pieceModels) +{ + auto& bd = getBoard(); + auto nuPieces = bd.get_nu_uniq_pieces(); + pieceModels.clear(); + pieceModels.reserve(nuPieces); + for (Piece::IntType i = 0; i < nuPieces; ++i) + { + Piece piece(i); + for (unsigned j = 0; j < bd.get_piece_info(piece).get_nu_instances(); + ++j) + pieceModels.append(new PieceModel(this, bd, piece, c)); + } +} + +void GameModel::deleteAllVar() +{ + if (! is_main_variation(m_game.get_current())) + emit positionAboutToChange(); + m_game.delete_all_variations(); + updateProperties(); +} + +bool GameModel::findMove(const PieceModel& pieceModel, const QString& state, + QPointF coord, Move& mv) const +{ + auto piece = pieceModel.getPiece(); + auto& bd = getBoard(); + if (piece.to_int() >= bd.get_nu_uniq_pieces()) + { + qWarning("GameModel::findMove: pieceModel invalid in game variant"); + return false; + } + auto transform = pieceModel.getTransform(state); + if (! transform) + { + qWarning("GameModel::findMove: transform not found"); + return false; + } + auto& info = bd.get_piece_info(piece); + PiecePoints piecePoints = info.get_points(); + transform->transform(piecePoints.begin(), piecePoints.end()); + auto boardType = bd.get_board_type(); + auto newPointType = transform->get_new_point_type(); + bool pointTypeChanged = + ((boardType == BoardType::trigon && newPointType == 1) + || (boardType == BoardType::trigon_3 && newPointType == 0)); + QPointF center(PieceModel::findCenter(bd, piecePoints, false)); + // Round y of center to a multiple of 0.5, works better in Trigon + center.setY(round(2 * center.y()) / 2); + int offX = static_cast(round(coord.x() - center.x())); + int offY = static_cast(round(coord.y() - center.y())); + auto& geo = bd.get_geometry(); + MovePoints points; + for (auto& p : piecePoints) + { + int x = p.x + offX; + int y = p.y + offY; + if (! geo.is_onboard(CoordPoint(x, y))) + return false; + auto pointType = geo.get_point_type(p); + auto boardPointType = geo.get_point_type(x, y); + if (! pointTypeChanged && pointType != boardPointType) + return false; + if (pointTypeChanged && pointType == boardPointType) + return false; + points.push_back(geo.get_point(x, y)); + } + return bd.find_move(points, piece, mv); +} + +QString GameModel::getResultMessage() +{ + auto& bd = getBoard(); + auto nuPlayers = bd.get_nu_players(); + bool breakTies = (bd.get_piece_set() == PieceSet::callisto); + if (m_nuColors == 2) + { + auto score = m_points0 - m_points1; + if (score == 1) + return tr("Blue wins with 1 point."); + if (score > 0) + return tr("Blue wins with %1 points.").arg(score); + if (score == -1) + return tr("Green wins with 1 point."); + if (score < 0) + return tr("Green wins with %1 points.").arg(-score); + if (breakTies) + return tr("Green wins (tie resolved)."); + return tr("Game ends in a tie."); + } + if (m_nuColors == 4 && nuPlayers == 2) + { + auto score = m_points0 + m_points2 - m_points1 - m_points3; + if (score == 1) + return tr("Blue/Red wins with 1 point."); + if (score > 0) + return tr("Blue/Red wins with %1 points.").arg(score); + if (score == -1) + return tr("Yellow/Green wins with 1 point."); + if (score < 0) + return tr("Yellow/Green wins with %1 points.").arg(-score); + if (breakTies) + return tr("Yellow/Green wins (tie resolved)."); + return tr("Game ends in a tie."); + } + if (nuPlayers == 3) + { + auto maxPoints = max(max(m_points0, m_points1), m_points2); + unsigned nuWinners = 0; + if (m_points0 == maxPoints) + ++nuWinners; + if (m_points1 == maxPoints) + ++nuWinners; + if (m_points2 == maxPoints) + ++nuWinners; + if (m_points0 == maxPoints && nuWinners == 1) + return tr("Blue wins."); + if (m_points1 == maxPoints && nuWinners == 1) + return tr("Yellow wins."); + if (m_points2 == maxPoints && nuWinners == 1) + return tr("Red wins."); + if (m_points2 == maxPoints && breakTies) + return tr("Red wins (tie resolved)."); + if (m_points1 == maxPoints && breakTies) + return tr("Yellow wins (tie resolved)."); + if (m_points0 == maxPoints && m_points1 == maxPoints && nuWinners == 2) + return tr("Game ends in a tie between Blue and Yellow."); + if (m_points0 == maxPoints && m_points2 == maxPoints && nuWinners == 2) + return tr("Game ends in a tie between Blue and Red."); + if (nuWinners == 2) + return tr("Game ends in a tie between Yellow and Red."); + return tr("Game ends in a tie between all players."); + } + auto maxPoints = max(max(m_points0, m_points1), max(m_points2, m_points3)); + unsigned nuWinners = 0; + if (m_points0 == maxPoints) + ++nuWinners; + if (m_points1 == maxPoints) + ++nuWinners; + if (m_points2 == maxPoints) + ++nuWinners; + if (m_points3 == maxPoints) + ++nuWinners; + if (m_points0 == maxPoints && nuWinners == 1) + return tr("Blue wins."); + if (m_points1 == maxPoints && nuWinners == 1) + return tr("Yellow wins."); + if (m_points2 == maxPoints && nuWinners == 1) + return tr("Red wins."); + if (m_points3 == maxPoints && nuWinners == 1) + return tr("Green wins."); + if (m_points3 == maxPoints && breakTies) + return tr("Green wins (tie resolved)."); + if (m_points2 == maxPoints && breakTies) + return tr("Red wins (tie resolved)."); + if (m_points1 == maxPoints && breakTies) + return tr("Yellow wins (tie resolved)."); + if (m_points0 == maxPoints && m_points1 == maxPoints + && m_points2 == maxPoints && nuWinners == 3) + return tr("Game ends in a tie between Blue, Yellow and Red."); + if (m_points0 == maxPoints && m_points1 == maxPoints + && m_points3 == maxPoints && nuWinners == 3) + return tr("Game ends in a tie between Blue, Yellow and Green."); + if (m_points0 == maxPoints && m_points2 == maxPoints + && m_points3 == maxPoints && nuWinners == 3) + return tr("Game ends in a tie between Blue, Red and Green."); + if (nuWinners == 3) + return tr("Game ends in a tie between Yellow, Red and Green."); + if (m_points0 == maxPoints && m_points1 == maxPoints && nuWinners == 2) + return tr("Game ends in a tie between Blue and Yellow."); + if (m_points0 == maxPoints && m_points2 == maxPoints && nuWinners == 2) + return tr("Game ends in a tie between Blue and Red."); + if (nuWinners == 2) + return tr("Game ends in a tie between Yellow and Red."); + return tr("Game ends in a tie between all players."); +} + +Variant GameModel::getInitialGameVariant() +{ + QSettings settings; + auto variantString = settings.value("variant", "").toString(); + Variant variant; + if (! parse_variant_id(variantString.toLocal8Bit().constData(), variant)) + variant = Variant::duo; + return variant; +} + +QList& GameModel::getPieceModels(Color c) +{ + if (c == Color(0)) + return m_pieceModels0; + else if (c == Color(1)) + return m_pieceModels1; + else if (c == Color(2)) + return m_pieceModels2; + else + return m_pieceModels3; +} + +void GameModel::goBackward() +{ + gotoNode(m_game.get_current().get_parent_or_null()); +} + +void GameModel::goBeginning() +{ + gotoNode(m_game.get_root()); +} + +void GameModel::goEnd() +{ + gotoNode(get_last_node(m_game.get_current())); +} + +void GameModel::goForward() +{ + gotoNode(m_game.get_current().get_first_child_or_null()); +} + +void GameModel::goNextVar() +{ + gotoNode(m_game.get_current().get_sibling()); +} + +void GameModel::goPrevVar() +{ + gotoNode(m_game.get_current().get_previous_sibling()); +} + +void GameModel::gotoNode(const SgfNode& node) +{ + if (&node == &m_game.get_current()) + return; + emit positionAboutToChange(); + try + { + m_game.goto_node(node); + } + catch (const InvalidTree&) + { + } + updateProperties(); +} + +void GameModel::gotoNode(const SgfNode* node) +{ + if (node) + gotoNode(*node); +} + +void GameModel::initGameVariant(const QString& gameVariant) +{ + Variant variant; + if (! parse_variant_id(gameVariant.toLocal8Bit().constData(), variant)) + { + qWarning("GameModel: invalid game variant"); + return; + } + if (m_game.get_variant() != variant) + m_game.init(variant); + auto& bd = getBoard(); + set(m_nuColors, static_cast(bd.get_nu_colors()), + &GameModel::nuColorsChanged); + m_lastMovePieceModel = nullptr; + createPieceModels(); + m_gameVariant = gameVariant; + emit gameVariantChanged(gameVariant); + updateProperties(); +} + +bool GameModel::isLegalPos(PieceModel* pieceModel, const QString& state, + QPointF coord) const +{ + Move mv; + if (! findMove(*pieceModel, state, coord, mv)) + return false; + Color c(static_cast(pieceModel->color())); + bool result = getBoard().is_legal(c, mv); + return result; +} + +bool GameModel::loadAutoSave() +{ + QSettings settings; + auto s = settings.value("autosave", "").toByteArray(); + istringstream in(s.constData()); + if (! open(in)) + return false; + m_game.set_modified(); + return true; +} + +void GameModel::makeMainVar() +{ + m_game.make_main_variation(); + updateProperties(); +} + +void GameModel::moveDownVar() +{ + m_game.move_down_variation(); + updateProperties(); +} + +void GameModel::moveUpVar() +{ + m_game.move_up_variation(); + updateProperties(); +} + +void GameModel::nextColor() +{ + emit positionAboutToChange(); + auto& bd = getBoard(); + m_game.set_to_play(bd.get_next(bd.get_to_play())); + updateProperties(); +} + +void GameModel::newGame() +{ + emit positionAboutToChange(); + m_game.init(); + for (auto pieceModel : m_pieceModels0) + pieceModel->setDefaultState(); + for (auto pieceModel : m_pieceModels1) + pieceModel->setDefaultState(); + for (auto pieceModel : m_pieceModels2) + pieceModel->setDefaultState(); + for (auto pieceModel : m_pieceModels3) + pieceModel->setDefaultState(); + updateProperties(); +} + +bool GameModel::open(istream& in) +{ + try + { + TreeReader reader; + reader.read(in); + auto root = reader.get_tree_transfer_ownership(); + emit positionAboutToChange(); + m_game.init(root); + auto variant = to_string_id(m_game.get_variant()); + if (variant != m_gameVariant) + initGameVariant(variant); + goEnd(); + updateProperties(); + QSettings settings; + settings.remove("autosave"); + } + catch (const runtime_error& e) + { + m_lastInputOutputError = QString::fromLocal8Bit(e.what()); + return false; + } + return true; +} + +bool GameModel::open(const QString& file) +{ + ifstream in(file.toLocal8Bit().constData()); + if (! in) + { + m_lastInputOutputError = QString::fromLocal8Bit(strerror(errno)); + return false; + } + return open(in); +} + +QQmlListProperty GameModel::pieceModels0() +{ + return QQmlListProperty(this, m_pieceModels0); +} + +QQmlListProperty GameModel::pieceModels1() +{ + return QQmlListProperty(this, m_pieceModels1); +} + +QQmlListProperty GameModel::pieceModels2() +{ + return QQmlListProperty(this, m_pieceModels2); +} + +QQmlListProperty GameModel::pieceModels3() +{ + return QQmlListProperty(this, m_pieceModels3); +} + +void GameModel::playMove(int move) +{ + Move mv(static_cast(move)); + if (mv.is_null()) + return; + emit positionAboutToChange(); + m_game.play(m_game.get_to_play(), mv, false); + updateProperties(); +} + +void GameModel::playPiece(PieceModel* pieceModel, QPointF coord) +{ + Color c(static_cast(pieceModel->color())); + Move mv; + if (! findMove(*pieceModel, pieceModel->state(), coord, mv)) + { + qWarning("GameModel::play: illegal move"); + return; + } + emit positionAboutToChange(); + preparePieceGameCoord(pieceModel, mv); + pieceModel->setIsPlayed(true); + preparePieceTransform(pieceModel, mv); + m_game.play(c, mv, false); + updateProperties(); +} + +PieceModel* GameModel::preparePiece(int color, int move) +{ + Move mv(static_cast(move)); + Color c(static_cast(color)); + Piece piece = getBoard().get_move_piece(mv); + for (auto pieceModel : getPieceModels(c)) + if (pieceModel->getPiece() == piece && ! pieceModel->isPlayed()) + { + preparePieceTransform(pieceModel, mv); + preparePieceGameCoord(pieceModel, mv); + return pieceModel; + } + return nullptr; +} + +void GameModel::preparePieceGameCoord(PieceModel* pieceModel, Move mv) +{ + pieceModel->setGameCoord(getGameCoord(getBoard(), mv)); +} + +void GameModel::preparePieceTransform(PieceModel* pieceModel, Move mv) +{ + auto& bd = getBoard(); + auto transform = bd.find_transform(mv); + auto& pieceInfo = bd.get_piece_info(bd.get_move_piece(mv)); + if (! compareTransform(pieceInfo, pieceModel->getTransform(), transform)) + pieceModel->setTransform(transform); +} + +bool GameModel::save(const QString& file) +{ + ofstream out(file.toLocal8Bit().constData()); + PentobiTreeWriter writer(out, m_game.get_tree()); + writer.set_indent(1); + writer.write(); + if (! out) + { + m_lastInputOutputError = QString::fromLocal8Bit(strerror(errno)); + return false; + } + m_game.clear_modified(); + return true; +} + +template +void GameModel::set(T& target, const T& value, + void (GameModel::*changedSignal)(T)) +{ + if (target != value) + { + target = value; + emit (this->*changedSignal)(value); + } +} + +void GameModel::truncate() +{ + if (! m_game.get_current().has_parent()) + return; + emit positionAboutToChange(); + m_game.truncate(); + updateProperties(); +} + +void GameModel::truncateChildren() +{ + m_game.truncate_children(); + updateProperties(); +} + +void GameModel::undo() +{ + if (! m_canUndo) + return; + emit positionAboutToChange(); + m_game.undo(); + updateProperties(); +} + +/** Helper function for updateProperties() */ +PieceModel* GameModel::updatePiece(Color c, Move mv, + array& isPlayed) +{ + auto& bd = getBoard(); + Piece piece = bd.get_move_piece(mv); + auto& pieceInfo = bd.get_piece_info(piece); + auto gameCoord = getGameCoord(bd, mv); + auto transform = bd.find_transform(mv); + auto& pieceModels = getPieceModels(c); + // Prefer piece models already played with the given gameCoord and + // transform because class Board doesn't make a distinction between + // instances of the same piece (in Junior) and we want to avoid + // unwanted piece movement animations to switch instances. + for (int i = 0; i < pieceModels.length(); ++i) + if (pieceModels[i]->getPiece() == piece + && pieceModels[i]->isPlayed() + && compareGameCoord(pieceModels[i]->gameCoord(), gameCoord) + && compareTransform(pieceInfo, pieceModels[i]->getTransform(), + transform)) + { + isPlayed[i] = true; + return pieceModels[i]; + } + for (int i = 0; i < pieceModels.length(); ++i) + if (pieceModels[i]->getPiece() == piece && ! isPlayed[i]) + { + isPlayed[i] = true; + // Order is important: isPlayed will trigger an animation to move + // the piece, so it needs to be set after gameCoord. + pieceModels[i]->setGameCoord(gameCoord); + pieceModels[i]->setIsPlayed(true); + pieceModels[i]->setTransform(transform); + return pieceModels[i]; + } + LIBBOARDGAME_ASSERT(false); + return nullptr; +} + +void GameModel::updateProperties() +{ + auto& bd = getBoard(); + auto& geo = bd.get_geometry(); + auto& tree = m_game.get_tree(); + bool isTrigon = (bd.get_piece_set() == PieceSet::trigon); + set(m_points0, bd.get_points(Color(0)), &GameModel::points0Changed); + set(m_points1, bd.get_points(Color(1)), &GameModel::points1Changed); + set(m_bonus0, bd.get_bonus(Color(0)), &GameModel::bonus0Changed); + set(m_bonus1, bd.get_bonus(Color(1)), &GameModel::bonus1Changed); + set(m_hasMoves0, bd.has_moves(Color(0)), &GameModel::hasMoves0Changed); + set(m_hasMoves1, bd.has_moves(Color(1)), &GameModel::hasMoves1Changed); + bool isFirstPieceAny = false; + if (m_nuColors > 2) + { + set(m_points2, bd.get_points(Color(2)), &GameModel::points2Changed); + set(m_bonus2, bd.get_bonus(Color(2)), &GameModel::bonus2Changed); + set(m_hasMoves2, bd.has_moves(Color(2)), &GameModel::hasMoves2Changed); + } + if (m_nuColors > 3) + { + set(m_points3, bd.get_points(Color(3)), &GameModel::points3Changed); + set(m_bonus3, bd.get_bonus(Color(3)), &GameModel::bonus3Changed); + set(m_hasMoves3, bd.has_moves(Color(3)), &GameModel::hasMoves3Changed); + } + m_tmpPoints.clear(); + if (bd.is_first_piece(Color(0))) + { + isFirstPieceAny = true; + if (! isTrigon) + for (Point p : bd.get_starting_points(Color(0))) + m_tmpPoints.append(QPointF(geo.get_x(p), geo.get_y(p))); + } + set(m_startingPoints0, m_tmpPoints, &GameModel::startingPoints0Changed); + m_tmpPoints.clear(); + if (bd.is_first_piece(Color(1))) + { + isFirstPieceAny = true; + if (! isTrigon) + for (Point p : bd.get_starting_points(Color(1))) + m_tmpPoints.append(QPointF(geo.get_x(p), geo.get_y(p))); + } + set(m_startingPoints1, m_tmpPoints, &GameModel::startingPoints1Changed); + m_tmpPoints.clear(); + if (m_nuColors > 2 && bd.is_first_piece(Color(2))) + { + isFirstPieceAny = true; + if (! isTrigon) + for (Point p : bd.get_starting_points(Color(2))) + m_tmpPoints.append(QPointF(geo.get_x(p), geo.get_y(p))); + } + set(m_startingPoints2, m_tmpPoints, &GameModel::startingPoints2Changed); + m_tmpPoints.clear(); + if (m_nuColors > 3 && bd.is_first_piece(Color(3))) + { + isFirstPieceAny = true; + if (! isTrigon) + for (Point p : bd.get_starting_points(Color(3))) + m_tmpPoints.append(QPointF(geo.get_x(p), geo.get_y(p))); + } + set(m_startingPoints3, m_tmpPoints, &GameModel::startingPoints3Changed); + m_tmpPoints.clear(); + if (isTrigon && isFirstPieceAny) + for (Point p : bd.get_starting_points(Color(0))) + m_tmpPoints.append(QPointF(geo.get_x(p), geo.get_y(p))); + set(m_startingPointsAll, m_tmpPoints, + &GameModel::startingPointsAllChanged); + auto& current = m_game.get_current(); + set(m_canUndo, + ! current.has_children() && tree.has_move_ignore_invalid(current) + && current.has_parent(), + &GameModel::canUndoChanged); + set(m_canGoForward, current.has_children(), + &GameModel::canGoForwardChanged); + set(m_canGoBackward, current.has_parent(), + &GameModel::canGoBackwardChanged); + set(m_hasPrevVar, (current.get_previous_sibling() != nullptr), + &GameModel::hasPrevVarChanged); + set(m_hasNextVar, (current.get_sibling() != nullptr), + &GameModel::hasNextVarChanged); + set(m_hasVariations, tree.has_variations(), + &GameModel::hasVariationsChanged); + set(m_isMainVar, is_main_variation(current), + &GameModel::isMainVarChanged); + auto positionInfo + = QString::fromLocal8Bit(get_position_info(tree, current).c_str()); + if (positionInfo.isEmpty()) + positionInfo = bd.has_setup() ? tr("(Setup)") : tr("(No moves)"); + else + { + positionInfo = tr("Move %1").arg(positionInfo); + if (bd.get_nu_moves() == 0 && bd.has_setup()) + { + positionInfo.append(' '); + positionInfo.append(tr("(Setup)")); + } + } + set(m_positionInfo, positionInfo, &GameModel::positionInfoChanged); + bool isGameOver = true; + for (Color c : bd.get_colors()) + if (bd.has_moves(c)) + { + isGameOver = false; + break; + } + set(m_isGameOver, isGameOver, &GameModel::isGameOverChanged); + set(m_isGameEmpty, libboardgame_sgf::util::is_empty(tree), + &GameModel::isGameEmptyChanged); + + ColorMap> isPlayed; + for (Color c : bd.get_colors()) + { + isPlayed[c].fill(false); + for (Move mv : bd.get_setup().placements[c]) + updatePiece(c, mv, isPlayed[c]); + } + PieceModel* lastMovePieceModel = nullptr; + for (unsigned i = 0; i < bd.get_nu_moves(); ++i) + { + auto mv = bd.get_move(i); + auto c = mv.color; + lastMovePieceModel = updatePiece(c, mv.move, isPlayed[c]); + } + if (lastMovePieceModel != m_lastMovePieceModel) + { + if (m_lastMovePieceModel != nullptr) + m_lastMovePieceModel->setIsLastMove(false); + if (lastMovePieceModel != nullptr) + lastMovePieceModel->setIsLastMove(true); + m_lastMovePieceModel = lastMovePieceModel; + } + for (Color c : bd.get_colors()) + { + auto& pieceModels = getPieceModels(c); + for (int i = 0; i < pieceModels.length(); ++i) + if (! isPlayed[c][i] && pieceModels[i]->isPlayed()) + { + pieceModels[i]->setDefaultState(); + pieceModels[i]->setIsPlayed(false); + } + } + + set(m_toPlay, m_isGameOver ? 0 : bd.get_effective_to_play().to_int(), + &GameModel::toPlayChanged); + set(m_altPlayer, + bd.get_variant() == Variant::classic_3 ? bd.get_alt_player() : 0, + &GameModel::altPlayerChanged); + + emit positionChanged(); +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi_qml/GameModel.h b/src/pentobi_qml/GameModel.h new file mode 100644 index 0000000..f192b40 --- /dev/null +++ b/src/pentobi_qml/GameModel.h @@ -0,0 +1,325 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_qml/GameModel.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_QML_GAME_MODEL_H +#define PENTOBI_QML_GAME_MODEL_H + +#include +#include "PieceModel.h" +#include "libpentobi_base/Game.h" + +using namespace std; +using libboardgame_sgf::SgfNode; +using libpentobi_base::Board; +using libpentobi_base::Game; +using libpentobi_base::Move; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +class GameModel + : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString gameVariant MEMBER m_gameVariant + NOTIFY gameVariantChanged) + Q_PROPERTY(QString positionInfo MEMBER m_positionInfo + NOTIFY positionInfoChanged) + Q_PROPERTY(QString lastInputOutputError MEMBER m_lastInputOutputError) + Q_PROPERTY(int nuColors MEMBER m_nuColors NOTIFY nuColorsChanged) + Q_PROPERTY(int toPlay MEMBER m_toPlay NOTIFY toPlayChanged) + Q_PROPERTY(int altPlayer MEMBER m_altPlayer NOTIFY altPlayerChanged) + Q_PROPERTY(float points0 MEMBER m_points0 NOTIFY points0Changed) + Q_PROPERTY(float points1 MEMBER m_points1 NOTIFY points1Changed) + Q_PROPERTY(float points2 MEMBER m_points2 NOTIFY points2Changed) + Q_PROPERTY(float points3 MEMBER m_points3 NOTIFY points3Changed) + Q_PROPERTY(float bonus0 MEMBER m_bonus0 NOTIFY bonus0Changed) + Q_PROPERTY(float bonus1 MEMBER m_bonus1 NOTIFY bonus1Changed) + Q_PROPERTY(float bonus2 MEMBER m_bonus2 NOTIFY bonus2Changed) + Q_PROPERTY(float bonus3 MEMBER m_bonus3 NOTIFY bonus3Changed) + Q_PROPERTY(bool hasMoves0 MEMBER m_hasMoves0 NOTIFY hasMoves0Changed) + Q_PROPERTY(bool hasMoves1 MEMBER m_hasMoves1 NOTIFY hasMoves1Changed) + Q_PROPERTY(bool hasMoves2 MEMBER m_hasMoves2 NOTIFY hasMoves2Changed) + Q_PROPERTY(bool hasMoves3 MEMBER m_hasMoves3 NOTIFY hasMoves3Changed) + Q_PROPERTY(bool isGameOver MEMBER m_isGameOver NOTIFY isGameOverChanged) + Q_PROPERTY(bool isGameEmpty MEMBER m_isGameEmpty NOTIFY isGameEmptyChanged) + Q_PROPERTY(bool canUndo MEMBER m_canUndo NOTIFY canUndoChanged) + Q_PROPERTY(bool canGoBackward MEMBER m_canGoBackward + NOTIFY canGoBackwardChanged) + Q_PROPERTY(bool canGoForward MEMBER m_canGoForward + NOTIFY canGoForwardChanged) + Q_PROPERTY(bool hasPrevVar MEMBER m_hasPrevVar NOTIFY hasPrevVarChanged) + Q_PROPERTY(bool hasNextVar MEMBER m_hasNextVar NOTIFY hasNextVarChanged) + Q_PROPERTY(bool hasVariations MEMBER m_hasVariations + NOTIFY hasVariationsChanged) + Q_PROPERTY(bool isMainVar MEMBER m_isMainVar NOTIFY isMainVarChanged) + Q_PROPERTY(QQmlListProperty pieceModels0 READ pieceModels0) + Q_PROPERTY(QQmlListProperty pieceModels1 READ pieceModels1) + Q_PROPERTY(QQmlListProperty pieceModels2 READ pieceModels2) + Q_PROPERTY(QQmlListProperty pieceModels3 READ pieceModels3) + Q_PROPERTY(QVariantList startingPoints0 MEMBER m_startingPoints0 + NOTIFY startingPoints0Changed) + Q_PROPERTY(QVariantList startingPoints1 MEMBER m_startingPoints1 + NOTIFY startingPoints1Changed) + Q_PROPERTY(QVariantList startingPoints2 MEMBER m_startingPoints2 + NOTIFY startingPoints2Changed) + Q_PROPERTY(QVariantList startingPoints3 MEMBER m_startingPoints3 + NOTIFY startingPoints3Changed) + Q_PROPERTY(QVariantList startingPointsAll MEMBER m_startingPointsAll + NOTIFY startingPointsAllChanged) + +public: + static Variant getInitialGameVariant(); + + explicit GameModel(QObject* parent = nullptr); + + Q_INVOKABLE void deleteAllVar(); + + Q_INVOKABLE bool isLegalPos(PieceModel* pieceModel, const QString& state, + QPointF coord) const; + + Q_INVOKABLE void nextColor(); + + Q_INVOKABLE void playPiece(PieceModel* pieceModel, QPointF coord); + + Q_INVOKABLE void playMove(int move); + + Q_INVOKABLE void newGame(); + + Q_INVOKABLE void undo(); + + Q_INVOKABLE void goBeginning(); + + Q_INVOKABLE void goBackward(); + + Q_INVOKABLE void goForward(); + + Q_INVOKABLE void goEnd(); + + Q_INVOKABLE void goNextVar(); + + Q_INVOKABLE void goPrevVar(); + + Q_INVOKABLE void backToMainVar(); + + Q_INVOKABLE void initGameVariant(const QString& gameVariant); + + Q_INVOKABLE void autoSave(); + + Q_INVOKABLE bool loadAutoSave(); + + /** Find the piece model for a given move and set its transform and game + coordinates accordingly but do not set its status to played yet. */ + Q_INVOKABLE PieceModel* preparePiece(int color, int move); + + Q_INVOKABLE bool save(const QString& file); + + Q_INVOKABLE bool open(const QString& file); + + Q_INVOKABLE void makeMainVar(); + + Q_INVOKABLE void moveDownVar(); + + Q_INVOKABLE void moveUpVar(); + + Q_INVOKABLE void truncate(); + + Q_INVOKABLE void truncateChildren(); + + Q_INVOKABLE QString getResultMessage(); + + QQmlListProperty pieceModels0(); + + QQmlListProperty pieceModels1(); + + QQmlListProperty pieceModels2(); + + QQmlListProperty pieceModels3(); + + const Board& getBoard() const { return m_game.get_board(); } + +signals: + /** Position is about to change due to new game or navigation or editing of + the game tree. */ + void positionAboutToChange(); + + /** Position changed due to new game or navigation or editing of the + game tree. */ + void positionChanged(); + + void toPlayChanged(int); + + void altPlayerChanged(int); + + void points0Changed(float); + + void points1Changed(float); + + void points2Changed(float); + + void points3Changed(float); + + void bonus0Changed(float); + + void bonus1Changed(float); + + void bonus2Changed(float); + + void bonus3Changed(float); + + void hasMoves0Changed(bool); + + void hasMoves1Changed(bool); + + void hasMoves2Changed(bool); + + void hasMoves3Changed(bool); + + void hasVariationsChanged(bool); + + void isGameOverChanged(bool); + + void isGameEmptyChanged(bool); + + void isMainVarChanged(bool); + + void canUndoChanged(bool); + + void canGoBackwardChanged(bool); + + void canGoForwardChanged(bool); + + void hasPrevVarChanged(bool); + + void hasNextVarChanged(bool); + + void gameVariantChanged(QString); + + void positionInfoChanged(QString); + + void nuColorsChanged(int); + + void startingPoints0Changed(QVariantList); + + void startingPoints1Changed(QVariantList); + + void startingPoints2Changed(QVariantList); + + void startingPoints3Changed(QVariantList); + + void startingPointsAllChanged(QVariantList); + +private: + Game m_game; + + QString m_gameVariant; + + QString m_positionInfo; + + QString m_lastInputOutputError; + + int m_nuColors; + + int m_toPlay = 0; + + int m_altPlayer = 0; + + float m_points0 = 0; + + float m_points1 = 0; + + float m_points2 = 0; + + float m_points3 = 0; + + float m_bonus0 = 0; + + float m_bonus1 = 0; + + float m_bonus2 = 0; + + float m_bonus3 = 0; + + bool m_hasMoves0 = true; + + bool m_hasMoves1 = true; + + bool m_hasMoves2 = true; + + bool m_hasMoves3 = true; + + bool m_hasVariations = false; + + bool m_isGameOver = false; + + bool m_isGameEmpty = true; + + bool m_canUndo = false; + + bool m_canGoForward = false; + + bool m_canGoBackward = false; + + bool m_hasPrevVar = false; + + bool m_hasNextVar = false; + + bool m_isMainVar = true; + + QList m_pieceModels0; + + QList m_pieceModels1; + + QList m_pieceModels2; + + QList m_pieceModels3; + + PieceModel* m_lastMovePieceModel = nullptr; + + QVariantList m_startingPoints0; + + QVariantList m_startingPoints1; + + QVariantList m_startingPoints2; + + QVariantList m_startingPoints3; + + QVariantList m_startingPointsAll; + + QVariantList m_tmpPoints; + + + void createPieceModels(); + + void createPieceModels(Color c, QList& pieceModels); + + bool findMove(const PieceModel& pieceModel, const QString& state, + QPointF coord, Move& mv) const; + + QList& getPieceModels(Color c); + + void gotoNode(const SgfNode& node); + + void gotoNode(const SgfNode* node); + + bool open(istream& in); + + void preparePieceGameCoord(PieceModel* pieceModel, Move mv); + + void preparePieceTransform(PieceModel* pieceModel, Move mv); + + template + void set(T& target, const T& value, void (GameModel::*changedSignal)(T)); + + PieceModel* updatePiece(Color c, Move mv, + array& isPlayed); + + void updateProperties(); +}; + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_QML_GAME_MODEL_H diff --git a/src/pentobi_qml/Main.cpp b/src/pentobi_qml/Main.cpp new file mode 100644 index 0000000..3153f11 --- /dev/null +++ b/src/pentobi_qml/Main.cpp @@ -0,0 +1,106 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_qml/Main.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include "GameModel.h" +#include "PlayerModel.h" +#include "libboardgame_util/Log.h" + +using libboardgame_util::RandomGenerator; + +//----------------------------------------------------------------------------- + +int main(int argc, char *argv[]) +{ + libboardgame_util::LogInitializer log_initializer; + QApplication app(argc, argv); + app.setOrganizationName("Pentobi"); + app.setApplicationName("Pentobi"); +#ifdef VERSION + app.setApplicationVersion(VERSION); +#endif + qmlRegisterType("pentobi", 1, 0, "GameModel"); + qmlRegisterType("pentobi", 1, 0, "PlayerModel"); + qmlRegisterInterface("PieceModel"); + QString locale = QLocale::system().name(); + QTranslator translatorPentobi; + translatorPentobi.load("qml_" + locale, ":qml/i18n"); + app.installTranslator(&translatorPentobi); + // The translation of standard buttons in QtQuick.Dialogs.MessageDialog + // is broken on Android (tested with Qt 5.5; QTBUG-43353), so we + // created our own file, which contains the translations we need. + QTranslator translatorQt; + translatorQt.load("replace_qtbase_" + locale, ":qml/i18n"); + app.installTranslator(&translatorQt); + QCommandLineParser parser; + QCommandLineOption optionNoBook("nobook"); + parser.addOption(optionNoBook); + QCommandLineOption optionNoDelay("nodelay"); + parser.addOption(optionNoDelay); + QCommandLineOption optionSeed("seed", "Set random seed to .", "n"); + parser.addOption(optionSeed); + QCommandLineOption optionThreads("threads", "Use threads (0=auto).", + "n"); + parser.addOption(optionThreads); + QCommandLineOption optionVerbose("verbose"); + parser.addOption(optionVerbose); + parser.process(app); + try + { +#if LIBBOARDGAME_DISABLE_LOG + if (parser.isSet(optionVerbose)) + throw runtime_error("This version of Pentobi was compiled" + " without support for logging."); +#else + if (! parser.isSet(optionVerbose)) + libboardgame_util::disable_logging(); +#endif + if (parser.isSet(optionNoBook)) + PlayerModel::noBook = true; + if (parser.isSet(optionNoDelay)) + PlayerModel::noDelay = true; + bool ok; + if (parser.isSet(optionSeed)) + { + auto seed = parser.value(optionSeed).toUInt(&ok); + if (! ok) + throw runtime_error("--seed must be a positive number"); + RandomGenerator::set_global_seed(seed); + } + if (parser.isSet(optionThreads)) + { + auto nuThreads = parser.value(optionThreads).toUInt(&ok); + if (! ok) + throw runtime_error("--threads must be a positive number"); + PlayerModel::nuThreads = nuThreads; + } + QQmlApplicationEngine engine(QUrl("qrc:///qml/Main.qml")); + return app.exec(); + } + catch (const bad_alloc&) + { + // bad_alloc is an expected error because the player requires a larger + // amount of memory. + QMessageBox::critical(nullptr, app.translate("main", "Pentobi"), + app.translate("main", "Not enough memory.")); + return 1; + } + catch (const exception& e) + { + cerr << "Error: " << e.what() << '\n'; + return 1; + } +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi_qml/Pentobi.pro b/src/pentobi_qml/Pentobi.pro new file mode 100644 index 0000000..b4ba11f --- /dev/null +++ b/src/pentobi_qml/Pentobi.pro @@ -0,0 +1,225 @@ +TEMPLATE = app + +QT += qml quick svg concurrent + +INCLUDEPATH += .. +CONFIG += c++11 +QMAKE_CXXFLAGS += -DVERSION=\"\\\"12.2\\\"\" +QMAKE_CXXFLAGS += -DPENTOBI_LOW_RESOURCES +android { + QMAKE_CXXFLAGS_RELEASE += -DLIBBOARDGAME_DISABLE_LOG +} +QMAKE_CXXFLAGS_DEBUG += -DLIBBOARDGAME_DEBUG +gcc { + QMAKE_CXXFLAGS_RELEASE -= -O + QMAKE_CXXFLAGS_RELEASE -= -O1 + QMAKE_CXXFLAGS_RELEASE -= -O2 + QMAKE_CXXFLAGS_RELEASE -= -O3 + QMAKE_CXXFLAGS_RELEASE -= -Os + QMAKE_CXXFLAGS_RELEASE *= -Ofast +} + +SOURCES += \ + GameModel.cpp \ + Main.cpp \ + PieceModel.cpp \ + PlayerModel.cpp \ + ../libboardgame_base/CoordPoint.cpp \ + ../libboardgame_base/Rating.cpp \ + ../libboardgame_base/RectTransform.cpp \ + ../libboardgame_base/StringRep.cpp \ + ../libboardgame_base/Transform.cpp \ + ../libboardgame_util/Abort.cpp \ + ../libboardgame_util/Assert.cpp \ + ../libboardgame_util/Barrier.cpp \ + ../libboardgame_util/CpuTimeSource.cpp \ + ../libboardgame_util/IntervalChecker.cpp \ + ../libboardgame_util/Log.cpp \ + ../libboardgame_util/RandomGenerator.cpp \ + ../libboardgame_util/StringUtil.cpp \ + ../libboardgame_util/TimeIntervalChecker.cpp \ + ../libboardgame_util/Timer.cpp \ + ../libboardgame_util/TimeSource.cpp \ + ../libboardgame_util/WallTimeSource.cpp \ + ../libboardgame_sgf/MissingProperty.cpp \ + ../libboardgame_sgf/Reader.cpp \ + ../libboardgame_sgf/SgfNode.cpp \ + ../libboardgame_sgf/SgfTree.cpp \ + ../libboardgame_sgf/SgfUtil.cpp \ + ../libboardgame_sgf/TreeReader.cpp \ + ../libboardgame_sgf/TreeWriter.cpp \ + ../libboardgame_sgf/Writer.cpp \ + ../libboardgame_sys/CpuTime.cpp \ + ../libboardgame_sys/Memory.cpp \ + ../libpentobi_base/Board.cpp \ + ../libpentobi_base/BoardConst.cpp \ + ../libpentobi_base/BoardUpdater.cpp \ + ../libpentobi_base/BoardUtil.cpp \ + ../libpentobi_base/Book.cpp \ + ../libpentobi_base/CallistoGeometry.cpp \ + ../libpentobi_base/Color.cpp \ + ../libpentobi_base/Game.cpp \ + ../libpentobi_base/NexosGeometry.cpp \ + ../libpentobi_base/NodeUtil.cpp \ + ../libpentobi_base/PentobiSgfUtil.cpp \ + ../libpentobi_base/PentobiTreeWriter.cpp \ + ../libpentobi_base/PieceInfo.cpp \ + ../libpentobi_base/PieceTransforms.cpp \ + ../libpentobi_base/PieceTransformsClassic.cpp \ + ../libpentobi_base/PieceTransformsTrigon.cpp \ + ../libpentobi_base/PointState.cpp \ + ../libpentobi_base/StartingPoints.cpp \ + ../libpentobi_base/SymmetricPoints.cpp \ + ../libpentobi_base/TreeUtil.cpp \ + ../libpentobi_base/TrigonGeometry.cpp \ + ../libpentobi_base/TrigonTransform.cpp \ + ../libpentobi_base/Variant.cpp \ + ../libpentobi_base/PlayerBase.cpp \ + ../libpentobi_base/PentobiTree.cpp \ + ../libpentobi_mcts/History.cpp \ + ../libpentobi_mcts/Player.cpp \ + ../libpentobi_mcts/PlayoutFeatures.cpp \ + ../libpentobi_mcts/PriorKnowledge.cpp \ + ../libpentobi_mcts/Search.cpp \ + ../libpentobi_mcts/SharedConst.cpp \ + ../libpentobi_mcts/State.cpp \ + ../libpentobi_mcts/Util.cpp \ + ../libpentobi_mcts/StateUtil.cpp + +RESOURCES += \ + ../books/pentobi_books.qrc \ + qml/themes/theme_shared.qrc \ + resources.qrc \ + translations.qrc + +android { + RESOURCES += \ + icons_android.qrc \ + qml/themes/theme_dark.qrc +} else { + RESOURCES += \ + ../pentobi/icons.qrc \ + qml/themes/theme_light.qrc +} + +# Default rules for deployment. +include(deployment.pri) + +HEADERS += \ + GameModel.h \ + PieceModel.h \ + PlayerModel.h \ + ../libboardgame_base/CoordPoint.h \ + ../libboardgame_base/Geometry.h \ + ../libboardgame_base/GeometryUtil.h \ + ../libboardgame_base/Grid.h \ + ../libboardgame_base/Marker.h \ + ../libboardgame_base/Point.h \ + ../libboardgame_base/PointTransform.h \ + ../libboardgame_base/Rating.h \ + ../libboardgame_base/RectGeometry.h \ + ../libboardgame_base/RectTransform.h \ + ../libboardgame_base/StringRep.h \ + ../libboardgame_base/Transform.h \ + ../libboardgame_mcts/Atomic.h \ + ../libboardgame_mcts/LastGoodReply.h \ + ../libboardgame_mcts/Node.h \ + ../libboardgame_mcts/PlayerMove.h \ + ../libboardgame_mcts/SearchBase.h \ + ../libboardgame_mcts/Tree.h \ + ../libboardgame_mcts/TreeUtil.h \ + ../libboardgame_util/Abort.h \ + ../libboardgame_util/ArrayList.h \ + ../libboardgame_util/Assert.h \ + ../libboardgame_util/Barrier.h \ + ../libboardgame_util/CpuTimeSource.h \ + ../libboardgame_util/FmtSaver.h \ + ../libboardgame_util/IntervalChecker.h \ + ../libboardgame_util/Log.h \ + ../libboardgame_util/MathUtil.h \ + ../libboardgame_util/Options.h \ + ../libboardgame_util/RandomGenerator.h \ + ../libboardgame_util/Statistics.h \ + ../libboardgame_util/StringUtil.h \ + ../libboardgame_util/TimeIntervalChecker.h \ + ../libboardgame_util/Timer.h \ + ../libboardgame_util/TimeSource.h \ + ../libboardgame_util/Unused.h \ + ../libboardgame_util/WallTimeSource.h \ + ../libboardgame_sgf/InvalidPropertyValue.h \ + ../libboardgame_sgf/InvalidTree.h \ + ../libboardgame_sgf/MissingProperty.h \ + ../libboardgame_sgf/Reader.h \ + ../libboardgame_sgf/SgfNode.h \ + ../libboardgame_sgf/SgfTree.h \ + ../libboardgame_sgf/SgfUtil.h \ + ../libboardgame_sgf/TreeReader.h \ + ../libboardgame_sgf/Writer.h \ + ../libboardgame_sys/Compiler.h \ + ../libboardgame_sys/CpuTime.h \ + ../libboardgame_sys/Memory.h \ + ../libpentobi_base/Board.h \ + ../libpentobi_base/BoardConst.h \ + ../libpentobi_base/BoardUpdater.h \ + ../libpentobi_base/BoardUtil.h \ + ../libpentobi_base/Book.h \ + ../libpentobi_base/Color.h \ + ../libpentobi_base/ColorMap.h \ + ../libpentobi_base/ColorMove.h \ + ../libpentobi_base/Game.h \ + ../libpentobi_base/Geometry.h \ + ../libpentobi_base/Grid.h \ + ../libpentobi_base/Marker.h \ + ../libpentobi_base/Move.h \ + ../libpentobi_base/MoveInfo.h \ + ../libpentobi_base/MoveList.h \ + ../libpentobi_base/MoveMarker.h \ + ../libpentobi_base/MovePoints.h \ + ../libpentobi_base/NexosGeometry.h \ + ../libpentobi_base/NodeUtil.h \ + ../libpentobi_base/PentobiTree.h \ + ../libpentobi_base/Piece.h \ + ../libpentobi_base/PieceInfo.h \ + ../libpentobi_base/PieceMap.h \ + ../libpentobi_base/PieceTransforms.h \ + ../libpentobi_base/PieceTransformsClassic.h \ + ../libpentobi_base/PieceTransformsTrigon.h \ + ../libpentobi_base/PlayerBase.h \ + ../libpentobi_base/Point.h \ + ../libpentobi_base/PointList.h \ + ../libpentobi_base/PointState.h \ + ../libpentobi_base/PrecompMoves.h \ + ../libpentobi_base/Setup.h \ + ../libpentobi_base/PentobiSgfUtil.h \ + ../libpentobi_base/StartingPoints.h \ + ../libpentobi_base/SymmetricPoints.h \ + ../libpentobi_base/TreeUtil.h \ + ../libpentobi_base/TrigonGeometry.h \ + ../libpentobi_base/TrigonTransform.h \ + ../libpentobi_base/Variant.h \ + ../libpentobi_mcts/Float.h \ + ../libpentobi_mcts/History.h \ + ../libpentobi_mcts/Player.h \ + ../libpentobi_mcts/PlayoutFeatures.h \ + ../libpentobi_mcts/PriorKnowledge.h \ + ../libpentobi_mcts/Search.h \ + ../libpentobi_mcts/SearchParamConst.h \ + ../libpentobi_mcts/SharedConst.h \ + ../libpentobi_mcts/State.h \ + ../libpentobi_mcts/StateUtil.h \ + ../libpentobi_mcts/Util.h + +lupdate_only { +SOURCES += \ + qml/*.qml \ + qml/*.js +} + +TRANSLATIONS += \ + qml/i18n/qml_de.ts \ + qml/i18n/replace_qtbase_de.ts + +OTHER_FILES += \ + android/AndroidManifest.xml + +ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android diff --git a/src/pentobi_qml/PieceModel.cpp b/src/pentobi_qml/PieceModel.cpp new file mode 100644 index 0000000..8c255b0 --- /dev/null +++ b/src/pentobi_qml/PieceModel.cpp @@ -0,0 +1,373 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_qml/PieceModel.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "PieceModel.h" + +#include +#include "libboardgame_base/RectTransform.h" +#include "libpentobi_base/TrigonTransform.h" + +using namespace std; +using libboardgame_base::ArrayList; +using libboardgame_base::CoordPoint; +using libboardgame_base::TransfIdentity; +using libboardgame_base::TransfRectRot90; +using libboardgame_base::TransfRectRot180; +using libboardgame_base::TransfRectRot270; +using libboardgame_base::TransfRectRefl; +using libboardgame_base::TransfRectRot90Refl; +using libboardgame_base::TransfRectRot180Refl; +using libboardgame_base::TransfRectRot270Refl; +using libpentobi_base::BoardType; +using libpentobi_base::PieceInfo; +using libpentobi_base::PieceSet; +using libpentobi_base::TransfTrigonIdentity; +using libpentobi_base::TransfTrigonRefl; +using libpentobi_base::TransfTrigonReflRot60; +using libpentobi_base::TransfTrigonReflRot120; +using libpentobi_base::TransfTrigonReflRot180; +using libpentobi_base::TransfTrigonReflRot240; +using libpentobi_base::TransfTrigonReflRot300; +using libpentobi_base::TransfTrigonRot60; +using libpentobi_base::TransfTrigonRot120; +using libpentobi_base::TransfTrigonRot180; +using libpentobi_base::TransfTrigonRot240; +using libpentobi_base::TransfTrigonRot300; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +PieceModel::PieceModel(QObject* parent, const Board& bd, Piece piece, Color c) + : QObject(parent), + m_bd(bd), + m_color(c), + m_piece(piece) +{ + auto& geo = bd.get_geometry(); + bool isNexos = (bd.get_piece_set() == PieceSet::nexos); + bool isCallisto = (bd.get_piece_set() == PieceSet::callisto); + auto& info = bd.get_piece_info(piece); + auto& points = info.get_points(); + m_elements.reserve(points.size()); + for (auto& p : points) + { + if (isNexos && geo.get_point_type(p) == 0) + continue; + m_elements.append(QPointF(p.x, p.y)); + } + if (isNexos) + { + ArrayList candidates; + for (auto& p : points) + { + auto pointType = geo.get_point_type(p); + if (pointType == 1) + { + candidates.include(CoordPoint(p.x - 1, p. y)); + candidates.include(CoordPoint(p.x + 1, p. y)); + } + else if (pointType == 2) + { + candidates.include(CoordPoint(p.x, p. y - 1)); + candidates.include(CoordPoint(p.x, p. y + 1)); + } + } + m_junctions.reserve(candidates.size()); + m_junctionType.reserve(candidates.size()); + for (auto& p : candidates) + { + bool hasLeft = points.contains(CoordPoint(p.x - 1, p. y)); + bool hasRight = points.contains(CoordPoint(p.x + 1, p. y)); + bool hasUp = points.contains(CoordPoint(p.x, p. y - 1)); + bool hasDown = points.contains(CoordPoint(p.x, p. y + 1)); + int junctionType; + if (hasLeft && hasRight && hasUp && hasDown) + junctionType = 0; + else if (hasRight && hasUp && hasDown) + junctionType = 1; + else if (hasLeft && hasUp && hasDown) + junctionType = 2; + else if (hasLeft && hasRight && hasDown) + junctionType = 3; + else if (hasLeft && hasRight && hasUp) + junctionType = 4; + else if (hasLeft && hasRight) + junctionType = 5; + else if (hasUp && hasDown) + junctionType = 6; + else if (hasLeft && hasUp) + junctionType = 7; + else if (hasLeft && hasDown) + junctionType = 8; + else if (hasRight && hasUp) + junctionType = 9; + else if (hasRight && hasDown) + junctionType = 10; + else + continue; + m_junctions.append(QPointF(p.x, p.y)); + m_junctionType.append(junctionType); + } + } + if (isCallisto) + for (auto& p : points) + { + bool hasRight = points.contains(CoordPoint(p.x + 1, p. y)); + bool hasDown = points.contains(CoordPoint(p.x, p. y + 1)); + int junctionType; + if (hasRight && hasDown) + junctionType = 0; + else if (hasRight) + junctionType = 1; + else if (hasDown) + junctionType = 2; + else + junctionType = 3; + m_junctionType.append(junctionType); + } + bool isOriginDownward = (m_bd.get_board_type() == BoardType::trigon_3); + m_center = findCenter(bd, points, isOriginDownward); + m_labelPos = QPointF(info.get_label_pos().x, info.get_label_pos().y); +} + +QPointF PieceModel::center() const +{ + return m_center; +} + +int PieceModel::color() +{ + return m_color.to_int(); +} + +QVariantList PieceModel::elements() +{ + return m_elements; +} + +void PieceModel::flipAcrossX() +{ + setTransform(m_bd.get_transforms().get_mirrored_vertically(getTransform())); +} + +void PieceModel::flipAcrossY() +{ + setTransform(m_bd.get_transforms().get_mirrored_horizontally(getTransform())); +} + +QPointF PieceModel::gameCoord() const +{ + return m_gameCoord; +} + +const Transform* PieceModel::getTransform(const QString& state) const +{ + auto variant = m_bd.get_variant(); + bool isTrigon = (variant == Variant::trigon || variant == Variant::trigon_2 + || variant == Variant::trigon_3); + auto& transforms = m_bd.get_transforms(); + // See comment in getTransform() about the mapping between states and + // transform classes. + if (state.isEmpty()) + return isTrigon ? transforms.find() + : transforms.find(); + if (state == QLatin1String("rot60")) + return transforms.find(); + if (state == QLatin1String("rot90")) + return transforms.find(); + if (state == QLatin1String("rot120")) + return transforms.find(); + if (state == QLatin1String("rot180")) + return isTrigon ? transforms.find() + : transforms.find(); + if (state == QLatin1String("rot240")) + return transforms.find(); + if (state == QLatin1String("rot270")) + return transforms.find(); + if (state == QLatin1String("rot300")) + return transforms.find(); + if (state == QLatin1String("flip")) + return isTrigon ? transforms.find() + : transforms.find(); + if (state == QLatin1String("rot60Flip")) + return transforms.find(); + if (state == QLatin1String("rot90Flip")) + return transforms.find(); + if (state == QLatin1String("rot120Flip")) + return transforms.find(); + if (state == QLatin1String("rot180Flip")) + return isTrigon ? transforms.find() + : transforms.find(); + if (state == QLatin1String("rot240Flip")) + return transforms.find(); + if (state == QLatin1String("rot270Flip")) + return transforms.find(); + if (state == QLatin1String("rot300Flip")) + return transforms.find(); + qWarning() << "PieceModel: unknown state " << m_state; + return transforms.find(); +} + +QPointF PieceModel::findCenter(const Board& bd, const PiecePoints& points, + bool isOriginDownward) +{ + auto pieceSet = bd.get_piece_set(); + bool isTrigon = (pieceSet == PieceSet::trigon); + bool isNexos = (pieceSet == PieceSet::nexos); + auto& geo = bd.get_geometry(); + qreal sumX = 0; + qreal sumY = 0; + qreal n = 0; + for (auto& p : points) + { + if (isNexos && geo.get_point_type(p) == 0) + continue; + ++n; + qreal centerX = p.x + 0.5; + qreal centerY; + if (isTrigon) + { + bool isDownward = + (geo.get_point_type(p) == (isOriginDownward ? 0 : 1)); + if (isDownward) + centerY = static_cast(p.y) + 1.f / 3; + else + centerY = static_cast(p.y) + 2.f / 3; + } + else + centerY = p.y + 0.5; + sumX += centerX; + sumY += centerY; + } + return QPointF(sumX / n, sumY / n); +} + +bool PieceModel::isLastMove() const +{ + return m_isLastMove; +} + +bool PieceModel::isPlayed() const +{ + return m_isPlayed; +} + +QVariantList PieceModel::junctions() +{ + return m_junctions; +} + +QVariantList PieceModel::junctionType() +{ + return m_junctionType; +} + +QPointF PieceModel::labelPos() const +{ + return m_labelPos; +} + +void PieceModel::rotateLeft() +{ + setTransform(m_bd.get_transforms().get_rotated_anticlockwise(getTransform())); +} + +void PieceModel::rotateRight() +{ + setTransform(m_bd.get_transforms().get_rotated_clockwise(getTransform())); +} + +void PieceModel::setGameCoord(QPointF gameCoord) +{ + if (m_gameCoord == gameCoord) + return; + m_gameCoord = gameCoord; + emit gameCoordChanged(gameCoord); +} + +void PieceModel::setIsLastMove(bool isLastMove) +{ + if (m_isLastMove == isLastMove) + return; + m_isLastMove = isLastMove; + emit isLastMoveChanged(isLastMove); +} + +void PieceModel::setIsPlayed(bool isPlayed) +{ + if (m_isPlayed == isPlayed) + return; + m_isPlayed = isPlayed; + emit isPlayedChanged(isPlayed); +} + +void PieceModel::setDefaultState() +{ + if (m_state.isEmpty()) + return; + m_state.clear(); + emit stateChanged(m_state); +} + +void PieceModel::setTransform(const Transform* transform) +{ + QString state; + // libboardgame_base uses a different convention for the order of flipping + // and rotation, so the names of the states and transform classes differ + // for flipped states. + if (dynamic_cast(transform) + || dynamic_cast(transform)) + ; + else if (dynamic_cast(transform)) + state = QLatin1String("rot60"); + else if (dynamic_cast(transform)) + state = QLatin1String("rot90"); + else if (dynamic_cast(transform)) + state = QLatin1String("rot120"); + else if (dynamic_cast(transform) + || dynamic_cast(transform)) + state = QLatin1String("rot180"); + else if (dynamic_cast(transform)) + state = QLatin1String("rot240"); + else if (dynamic_cast(transform)) + state = QLatin1String("rot270"); + else if (dynamic_cast(transform)) + state = QLatin1String("rot300"); + else if (dynamic_cast(transform) + || dynamic_cast(transform)) + state = QLatin1String("flip"); + else if (dynamic_cast(transform)) + state = QLatin1String("rot60Flip"); + else if (dynamic_cast(transform)) + state = QLatin1String("rot90Flip"); + else if (dynamic_cast(transform)) + state = QLatin1String("rot120Flip"); + else if (dynamic_cast(transform) + || dynamic_cast(transform)) + state = QLatin1String("rot180Flip"); + else if (dynamic_cast(transform)) + state = QLatin1String("rot240Flip"); + else if (dynamic_cast(transform)) + state = QLatin1String("rot270Flip"); + else if (dynamic_cast(transform)) + state = QLatin1String("rot300Flip"); + else + { + qWarning() << "Invalid Transform " << typeid(*transform).name(); + return; + } + if (m_state == state) + return; + m_state = state; + emit stateChanged(m_state); +} + +QString PieceModel::state() const +{ + return m_state; +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi_qml/PieceModel.h b/src/pentobi_qml/PieceModel.h new file mode 100644 index 0000000..dc344fd --- /dev/null +++ b/src/pentobi_qml/PieceModel.h @@ -0,0 +1,133 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_qml/PieceModel.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_QML_PIECE_MODEL_H +#define PENTOBI_QML_PIECE_MODEL_H + +#include +#include +#include +#include "libpentobi_base/Board.h" + +using libboardgame_base::Transform; +using libpentobi_base::Board; +using libpentobi_base::Color; +using libpentobi_base::Piece; +using libpentobi_base::PiecePoints; + +//----------------------------------------------------------------------------- + +class PieceModel + : public QObject +{ + Q_OBJECT + + Q_PROPERTY(int color READ color CONSTANT) + Q_PROPERTY(QVariantList elements READ elements CONSTANT) + Q_PROPERTY(QVariantList junctions READ junctions CONSTANT) + Q_PROPERTY(QVariantList junctionType READ junctionType CONSTANT) + Q_PROPERTY(QPointF center READ center CONSTANT) + Q_PROPERTY(QPointF labelPos READ labelPos CONSTANT) + Q_PROPERTY(QString state READ state NOTIFY stateChanged) + Q_PROPERTY(bool isPlayed READ isPlayed NOTIFY isPlayedChanged) + Q_PROPERTY(bool isLastMove READ isLastMove NOTIFY isLastMoveChanged) + Q_PROPERTY(QPointF gameCoord READ gameCoord NOTIFY gameCoordChanged) + +public: + static QPointF findCenter(const Board& bd, const PiecePoints& points, + bool isOriginDownward); + + PieceModel(QObject* parent, const Board& bd, Piece piece, Color c); + + int color(); + + /** List of QPointF instances with coordinates of piece elements. */ + QVariantList elements(); + + /** List of QPointF instances with coordinates of piece junctions. + Only used in Nexos. */ + QVariantList junctions(); + + /** List of integers determining the type of junctions. + In Nexos, this is the type of junction in junction(). In Callisto, it + is the information if the squares in elements() have a right and/or + down neighbor. See implementation for the meaning of the numbers. */ + QVariantList junctionType(); + + QPointF center() const; + + QPointF labelPos() const; + + QString state() const; + + bool isPlayed() const; + + bool isLastMove() const; + + QPointF gameCoord() const; + + Piece getPiece() const { return m_piece; } + + const Transform* getTransform(const QString& state) const; + + const Transform* getTransform() const { return getTransform(m_state); } + + void setDefaultState(); + + void setTransform(const Transform* transform); + + void setIsPlayed(bool isPlayed); + + void setIsLastMove(bool isLastMove); + + void setGameCoord(QPointF gameCoord); + + Q_INVOKABLE void rotateLeft(); + + Q_INVOKABLE void rotateRight(); + + Q_INVOKABLE void flipAcrossX(); + + Q_INVOKABLE void flipAcrossY(); + +signals: + void stateChanged(QString); + + void isPlayedChanged(bool); + + void isLastMoveChanged(bool); + + void gameCoordChanged(QPointF); + +private: + const Board& m_bd; + + Color m_color; + + Piece m_piece; + + bool m_isPlayed = false; + + bool m_isLastMove = false; + + QPointF m_gameCoord; + + QPointF m_center; + + QPointF m_labelPos; + + QVariantList m_elements; + + QVariantList m_junctions; + + QVariantList m_junctionType; + + QString m_state; +}; + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_QML_PIECE_MODEL_H diff --git a/src/pentobi_qml/PlayerModel.cpp b/src/pentobi_qml/PlayerModel.cpp new file mode 100644 index 0000000..a21cb98 --- /dev/null +++ b/src/pentobi_qml/PlayerModel.cpp @@ -0,0 +1,228 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_qml/PlayerModel.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "PlayerModel.h" + +#include +#include +#include +#include + +using namespace std; +using libboardgame_util::clear_abort; +using libboardgame_util::set_abort; + +//----------------------------------------------------------------------------- + +namespace { + +unsigned maxLevel = 7; + +void getLevel(QSettings& settings, const char* key, unsigned& level) +{ + level = settings.value(key, 1).toUInt(); + if (level < 1) + { + qDebug() << "PlayerModel: invalid level in settings:" << level; + level = 1; + } + else if (level > maxLevel) + { + qDebug() << "PlayerModel: level in settings too high, using level" + << maxLevel; + level = maxLevel; + } +} + +} // namespace + +//----------------------------------------------------------------------------- + +bool PlayerModel::noBook = false; + +bool PlayerModel::noDelay = false; + +unsigned PlayerModel::nuThreads = 0; + +PlayerModel::PlayerModel(QObject* parent) + : QObject(parent), + m_player(GameModel::getInitialGameVariant(), maxLevel, "", nuThreads) +{ + if (noBook) + m_player.set_use_book(false); + QSettings settings; + getLevel(settings, "level_classic", m_levelClassic); + getLevel(settings, "level_classic_2", m_levelClassic2); + getLevel(settings, "level_classic_3", m_levelClassic3); + getLevel(settings, "level_duo", m_levelDuo); + getLevel(settings, "level_trigon", m_levelTrigon); + getLevel(settings, "level_trigon_2", m_levelTrigon2); + getLevel(settings, "level_trigon_3", m_levelTrigon3); + getLevel(settings, "level_junior", m_levelJunior); + getLevel(settings, "level_nexos", m_levelNexos); + getLevel(settings, "level_nexos_2", m_levelNexos2); + getLevel(settings, "level_callisto", m_levelCallisto); + getLevel(settings, "level_callisto_2", m_levelCallisto2); + getLevel(settings, "level_callisto_3", m_levelCallisto3); + connect(&m_genMoveWatcher, SIGNAL(finished()), SLOT(genMoveFinished())); +} + +PlayerModel::~PlayerModel() +{ + cancelGenMove(); + QSettings settings; + settings.setValue("level_classic", m_levelClassic); + settings.setValue("level_classic_2", m_levelClassic2); + settings.setValue("level_classic_3", m_levelClassic3); + settings.setValue("level_duo", m_levelDuo); + settings.setValue("level_trigon", m_levelTrigon); + settings.setValue("level_trigon_2", m_levelTrigon2); + settings.setValue("level_trigon_3", m_levelTrigon3); + settings.setValue("level_junior", m_levelJunior); + settings.setValue("level_nexos", m_levelNexos); + settings.setValue("level_nexos_2", m_levelNexos2); + settings.setValue("level_callisto", m_levelCallisto); + settings.setValue("level_callisto_2", m_levelCallisto2); + settings.setValue("level_callisto_3", m_levelCallisto3); +} + +PlayerModel::GenMoveResult PlayerModel::asyncGenMove(GameModel* gm, + unsigned genMoveId) +{ + QElapsedTimer timer; + timer.start(); + auto& bd = gm->getBoard(); + GenMoveResult result; + result.genMoveId = genMoveId; + result.gameModel = gm; + result.move = m_player.genmove(bd, bd.get_effective_to_play()); + auto elapsed = timer.elapsed(); + // Enforce minimum thinking time of 1 sec + if (elapsed < 1000 && ! noDelay) + QThread::msleep(1000 - elapsed); + return result; +} + +void PlayerModel::cancelGenMove() +{ + if (! m_isGenMoveRunning) + return; + // After waitForFinished() returns, we can be sure that the move generation + // is no longer running, but we will still receive the finished event. + // Increasing m_genMoveId will make genMoveFinished() ignore the event. + ++m_genMoveId; + set_abort(); + m_genMoveWatcher.waitForFinished(); + setIsGenMoveRunning(false); +} + +void PlayerModel::genMoveFinished() +{ + auto result = m_genMoveWatcher.future().result(); + if (result.genMoveId != m_genMoveId) + // Callback from a canceled move generation + return; + setIsGenMoveRunning(false); + auto& bd = result.gameModel->getBoard(); + auto mv = result.move; + if (mv.is_null()) + { + qWarning("PlayerModel: failed to generate move"); + return; + } + Color c = bd.get_effective_to_play(); + if (! bd.is_legal(c, mv)) + { + qWarning("PlayerModel: player generated illegal move"); + return; + } + emit moveGenerated(mv.to_int()); +} + +void PlayerModel::loadBook(Variant variant) +{ + QFile file(QString(":/pentobi_books/book_%1.blksgf") + .arg(to_string_id(variant))); + if (! file.open(QIODevice::ReadOnly)) + { + qWarning() << "PlayerModel: could not open " << file.fileName(); + return; + } + QTextStream stream(&file); + QString text = stream.readAll(); + istringstream in(text.toLocal8Bit().constData()); + m_player.load_book(in); +} + +void PlayerModel::setIsGenMoveRunning(bool isGenMoveRunning) +{ + if (m_isGenMoveRunning == isGenMoveRunning) + return; + m_isGenMoveRunning = isGenMoveRunning; + emit isGenMoveRunningChanged(isGenMoveRunning); +} + +void PlayerModel::startGenMove(GameModel* gm) +{ + unsigned level; + switch (gm->getBoard().get_variant()) + { + case Variant::classic_2: + level = m_levelClassic2; + break; + case Variant::classic_3: + level = m_levelClassic3; + break; + case Variant::duo: + level = m_levelDuo; + break; + case Variant::trigon: + level = m_levelTrigon; + break; + case Variant::trigon_2: + level = m_levelTrigon2; + break; + case Variant::trigon_3: + level = m_levelTrigon3; + break; + case Variant::nexos: + level = m_levelNexos; + break; + case Variant::nexos_2: + level = m_levelNexos2; + break; + case Variant::callisto: + level = m_levelCallisto; + break; + case Variant::callisto_2: + level = m_levelCallisto2; + break; + case Variant::callisto_3: + level = m_levelCallisto3; + break; + default: + level = m_levelClassic; + } + startGenMoveAtLevel(gm, level); +} + +void PlayerModel::startGenMoveAtLevel(GameModel* gm, unsigned level) +{ + cancelGenMove(); + m_player.set_level(level); + auto variant = gm->getBoard().get_variant(); + if (! m_player.is_book_loaded(variant)) + loadBook(variant); + clear_abort(); + ++m_genMoveId; + QFuture future = + QtConcurrent::run(this, &PlayerModel::asyncGenMove, gm, + m_genMoveId); + m_genMoveWatcher.setFuture(future); + setIsGenMoveRunning(true); +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi_qml/PlayerModel.h b/src/pentobi_qml/PlayerModel.h new file mode 100644 index 0000000..b08cebe --- /dev/null +++ b/src/pentobi_qml/PlayerModel.h @@ -0,0 +1,173 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_qml/PlayerModel.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef PENTOBI_QML_PLAYER_MODEL_H +#define PENTOBI_QML_PLAYER_MODEL_H + +#include +#include "GameModel.h" +#include "libpentobi_mcts/Player.h" + +using namespace std; +using libpentobi_mcts::Player; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +class PlayerModel + : public QObject +{ + Q_OBJECT + Q_PROPERTY(unsigned levelClassic MEMBER m_levelClassic + NOTIFY levelClassicChanged) + Q_PROPERTY(unsigned levelClassic2 MEMBER m_levelClassic2 + NOTIFY levelClassic2Changed) + Q_PROPERTY(unsigned levelClassic3 MEMBER m_levelClassic3 + NOTIFY levelClassic3Changed) + Q_PROPERTY(unsigned levelDuo MEMBER m_levelDuo NOTIFY levelDuoChanged) + Q_PROPERTY(unsigned levelTrigon MEMBER m_levelTrigon + NOTIFY levelTrigonChanged) + Q_PROPERTY(unsigned levelTrigon2 MEMBER m_levelTrigon2 + NOTIFY levelTrigon2Changed) + Q_PROPERTY(unsigned levelTrigon3 MEMBER m_levelTrigon3 + NOTIFY levelTrigon3Changed) + Q_PROPERTY(unsigned levelJunior MEMBER m_levelJunior + NOTIFY levelJuniorChanged) + Q_PROPERTY(unsigned levelNexos MEMBER m_levelNexos NOTIFY + levelNexosChanged) + Q_PROPERTY(unsigned levelNexos2 MEMBER m_levelNexos2 NOTIFY + levelNexos2Changed) + Q_PROPERTY(unsigned levelCallisto MEMBER m_levelCallisto + NOTIFY levelCallistoChanged) + Q_PROPERTY(unsigned levelCallisto2 MEMBER m_levelCallisto2 + NOTIFY levelCallisto2Changed) + Q_PROPERTY(unsigned levelCallisto3 MEMBER m_levelCallisto3 + NOTIFY levelCallisto3Changed) + Q_PROPERTY(bool isGenMoveRunning MEMBER m_isGenMoveRunning + NOTIFY isGenMoveRunningChanged) + +public: + /** Global variable to disable opening books. */ + static bool noBook; + + /** Global variable to disable the minimum thinking time. */ + static bool noDelay; + + /** Global variable to set the number of threads the player is constructed + with. + The default value 0 means that the number of threads depends on the + hardware. */ + static unsigned nuThreads; + + + explicit PlayerModel(QObject* parent = nullptr); + + ~PlayerModel(); + + + /** Start a move generation in a background thread. + The state of the board model may not be changed until the move + generation was finished (computerPlayed signal) or aborted + with cancelGenMove() */ + Q_INVOKABLE void startGenMove(GameModel* gameModel); + + Q_INVOKABLE void startGenMoveAtLevel(GameModel* gameModel, unsigned level); + + /** Cancel the move generation in the background thread if one is + running. */ + Q_INVOKABLE void cancelGenMove(); + +signals: + void levelCallistoChanged(unsigned); + + void levelCallisto2Changed(unsigned); + + void levelCallisto3Changed(unsigned); + + void levelClassicChanged(unsigned); + + void levelClassic2Changed(unsigned); + + void levelClassic3Changed(unsigned); + + void levelDuoChanged(unsigned); + + void levelTrigonChanged(unsigned); + + void levelTrigon2Changed(unsigned); + + void levelTrigon3Changed(unsigned); + + void levelJuniorChanged(unsigned); + + void levelNexosChanged(unsigned); + + void levelNexos2Changed(unsigned); + + void isGenMoveRunningChanged(bool); + + void moveGenerated(int move); + +private: + struct GenMoveResult + { + Color color; + + Move move; + + unsigned genMoveId; + + GameModel* gameModel; + }; + + bool m_isGenMoveRunning = false; + + unsigned m_levelCallisto; + + unsigned m_levelCallisto2; + + unsigned m_levelCallisto3; + + unsigned m_levelClassic; + + unsigned m_levelClassic2; + + unsigned m_levelClassic3; + + unsigned m_levelDuo; + + unsigned m_levelTrigon; + + unsigned m_levelTrigon2; + + unsigned m_levelTrigon3; + + unsigned m_levelJunior; + + unsigned m_levelNexos; + + unsigned m_levelNexos2; + + unsigned m_genMoveId = 0; + + Player m_player; + + QFutureWatcher m_genMoveWatcher; + + + GenMoveResult asyncGenMove(GameModel* gm, unsigned genMoveId); + + void loadBook(Variant variant); + + void setIsGenMoveRunning(bool isGenMoveRunning); + +private slots: + void genMoveFinished(); +}; + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_QML_PLAYER_MODEL_H diff --git a/src/pentobi_qml/android/AndroidManifest.xml b/src/pentobi_qml/android/AndroidManifest.xml new file mode 100644 index 0000000..60bb1c7 --- /dev/null +++ b/src/pentobi_qml/android/AndroidManifest.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/android/res/drawable-hdpi/icon.png b/src/pentobi_qml/android/res/drawable-hdpi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4a5bd8e829e2870a33208fbda081a02416146025 GIT binary patch literal 409 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~aez;VE5pv&I-y-AsY@KQ=7-j9 zO57k^d`YbAxLW5;>(vYl2Sr3qY+^XWz;H!F<>&pIsM_N$LQqtY;8c^uI zr;B4q#NoFy4)PsR5O8sH(-a`#n4_tA3;cF3l%sw^;W-jnUz_2=I035yyE&P-%F zuA|BIg45F_=k&7IjX?@Y(Pnq0O|Ek`ZmebG^lM zu~_D5s9~0@SGq;*=!3ODRZPElIa;U8FP9GD>Eb literal 0 HcmV?d00001 diff --git a/src/pentobi_qml/android/res/drawable-mdpi/icon.png b/src/pentobi_qml/android/res/drawable-mdpi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4fb43977aea82dd4e26e6f77f5020d2bab2f4fd1 GIT binary patch literal 324 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-s#sNMdu0UERw96!QiDTCM(ArIj z8-$B5iIp8!>%3{bnt|b;OkH}&M25w;xW@MN(M*=9wUgGKN%Km^yT2MoE@=<$npiq~m zi(`n#@wd|q`5F`iSR5S~UH|^i_MH-~f2_c45o>VnLjj*<4lA{m`1mMP86;f%E-ZM- zveth`@w3nKxz};N=QLz6V`sl{r&aWfPVa9ozC=a0k7d3;CK~iRta&o4jkWEplhih4 zli98H8Hd`Lybos_NK@q9IO+A&BezW-+B({bztC(jY4LhxlVT<%=5VX8wDSbeRScf4 KelF{r5}E)#5N#O% literal 0 HcmV?d00001 diff --git a/src/pentobi_qml/android/res/drawable/splash.xml b/src/pentobi_qml/android/res/drawable/splash.xml new file mode 100644 index 0000000..32f67f9 --- /dev/null +++ b/src/pentobi_qml/android/res/drawable/splash.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/pentobi_qml/android/res/values/theme.xml b/src/pentobi_qml/android/res/values/theme.xml new file mode 100644 index 0000000..adec232 --- /dev/null +++ b/src/pentobi_qml/android/res/values/theme.xml @@ -0,0 +1,6 @@ + + + + diff --git a/src/pentobi_qml/android_icons_svg/icon48.svg b/src/pentobi_qml/android_icons_svg/icon48.svg new file mode 100644 index 0000000..655a58e --- /dev/null +++ b/src/pentobi_qml/android_icons_svg/icon48.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/android_icons_svg/icon72.svg b/src/pentobi_qml/android_icons_svg/icon72.svg new file mode 100644 index 0000000..3575ed4 --- /dev/null +++ b/src/pentobi_qml/android_icons_svg/icon72.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/deployment.pri b/src/pentobi_qml/deployment.pri new file mode 100644 index 0000000..5441b63 --- /dev/null +++ b/src/pentobi_qml/deployment.pri @@ -0,0 +1,27 @@ +android-no-sdk { + target.path = /data/user/qt + export(target.path) + INSTALLS += target +} else:android { + x86 { + target.path = /libs/x86 + } else: armeabi-v7a { + target.path = /libs/armeabi-v7a + } else { + target.path = /libs/armeabi + } + export(target.path) + INSTALLS += target +} else:unix { + isEmpty(target.path) { + qnx { + target.path = /tmp/$${TARGET}/bin + } else { + target.path = /opt/$${TARGET}/bin + } + export(target.path) + } + INSTALLS += target +} + +export(INSTALLS) diff --git a/src/pentobi_qml/icons_android.qrc b/src/pentobi_qml/icons_android.qrc new file mode 100644 index 0000000..c3a99c7 --- /dev/null +++ b/src/pentobi_qml/icons_android.qrc @@ -0,0 +1,15 @@ + + + qml/icons/menu.svg + qml/icons/pentobi-backward.svg + qml/icons/pentobi-beginning.svg + qml/icons/pentobi-computer-colors.svg + qml/icons/pentobi-end.svg + qml/icons/pentobi-forward.svg + qml/icons/pentobi-newgame.svg + qml/icons/pentobi-next-variation.svg + qml/icons/pentobi-play.svg + qml/icons/pentobi-previous-variation.svg + qml/icons/pentobi-undo.svg + + diff --git a/src/pentobi_qml/qml/.gitignore b/src/pentobi_qml/qml/.gitignore new file mode 100644 index 0000000..8df47d5 --- /dev/null +++ b/src/pentobi_qml/qml/.gitignore @@ -0,0 +1 @@ +*.qm diff --git a/src/pentobi_qml/qml/AndroidToolBar.qml b/src/pentobi_qml/qml/AndroidToolBar.qml new file mode 100644 index 0000000..f0bf836 --- /dev/null +++ b/src/pentobi_qml/qml/AndroidToolBar.qml @@ -0,0 +1,46 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.1 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.0 +import "Main.js" as Logic + +RowLayout { + function popupMenu() { menu.popup() } + + spacing: 0 + + Item { Layout.fillWidth: true } + AndroidToolButton { + imageSource: "icons/pentobi-newgame.svg" + visible: ! gameModel.isGameEmpty + onClicked: Logic.newGame() + } + AndroidToolButton { + visible: gameModel.canUndo + imageSource: "icons/pentobi-undo.svg" + onClicked: Logic.undo() + } + AndroidToolButton { + imageSource: "icons/pentobi-computer-colors.svg" + onClicked: Logic.showComputerColorDialog() + } + AndroidToolButton { + visible: ! gameModel.isGameOver + imageSource: "icons/pentobi-play.svg" + onClicked: Logic.computerPlay() + } + AndroidToolButton { + imageSource: "icons/menu.svg" + menu: menu + } + Menu { + id: menu + + MenuGame { } + MenuGo { } + MenuEdit { } + MenuComputer { } + MenuView { } + MenuHelp { } + } +} diff --git a/src/pentobi_qml/qml/AndroidToolButton.qml b/src/pentobi_qml/qml/AndroidToolButton.qml new file mode 100644 index 0000000..44f21b3 --- /dev/null +++ b/src/pentobi_qml/qml/AndroidToolButton.qml @@ -0,0 +1,18 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.1 +import QtQuick.Window 2.0 + +ToolButton { + property string imageSource + + Image { + // We currently use 22x22 SVG files, try to use 22x22 or 44x44, unless + // very high DPI + width: Screen.pixelDensity < 5 ? 22 : Screen.pixelDensity < 10 ? 44 : 5 * Screen.pixelDensity + height: width + sourceSize { width: width; height: height } + anchors.centerIn: parent + source: imageSource + cache: false + } +} diff --git a/src/pentobi_qml/qml/Board.qml b/src/pentobi_qml/qml/Board.qml new file mode 100644 index 0000000..3e818d9 --- /dev/null +++ b/src/pentobi_qml/qml/Board.qml @@ -0,0 +1,208 @@ +import QtQuick 2.0 + +Item { + id: root + + property string gameVariant + property bool isTrigon: gameVariant.indexOf("trigon") === 0 + property bool isNexos: gameVariant.indexOf("nexos") === 0 + property bool isCallisto: gameVariant.indexOf("callisto") === 0 + property int columns: { + switch (gameVariant) { + case "duo": + case "junior": + return 14 + case "callisto_2": + return 16 + case "trigon": + case "trigon_2": + return 35 + case "trigon_3": + return 31 + case "nexos": + case "nexos_2": + return 25 + default: + return 20 + } + } + property int rows: { + switch (gameVariant) { + case "duo": + case "junior": + return 14 + case "callisto_2": + return 16 + case "trigon": + case "trigon_2": + return 18 + case "trigon_3": + return 16 + case "nexos": + case "nexos_2": + return 25 + default: + return 20 + } + } + // Avoid fractional piece element sizes if the piece elements are squares + property real gridWidth: { + var sideLength + if (isTrigon) sideLength = Math.min(width, Math.sqrt(3) * height) + else sideLength = Math.min(width, height) + if (isTrigon) return sideLength / (columns + 1) + else if (isNexos) Math.floor(sideLength / (columns - 0.5)) + else return Math.floor(sideLength / columns) + } + property real gridHeight: { + if (isTrigon) return Math.sqrt(3) * gridWidth + else return gridWidth + } + property real startingPointSize: { + if (isTrigon) return 0.27 * gridHeight + if (isNexos) return 0.3 * gridHeight + return 0.35 * gridHeight + } + + function mapFromGameX(x) { + if (isTrigon) return image.x + (x + 0.5) * gridWidth + else if (isNexos) return image.x + (x - 0.25) * gridWidth + else return image.x + x * gridWidth + } + function mapFromGameY(y) { + if (isNexos) return image.y + (y - 0.25) * gridHeight + else return image.y + y * gridHeight + } + function mapToGame(pos) { + if (isTrigon) + return Qt.point((pos.x - image.x - 0.5 * gridWidth) / gridWidth, + (pos.y - image.y) / gridHeight) + else if (isNexos) + return Qt.point((pos.x - image.x + 0.25 * gridWidth) / gridWidth, + (pos.y - image.y + 0.25 * gridHeight) / gridHeight) + else + return Qt.point((pos.x - image.x) / gridWidth, + (pos.y - image.y) / gridHeight) + } + function getCenterYTrigon(pos) { + + var isDownward = ((pos.x % 2 == 0) != (pos.y % 2 == 0)) + if (gameVariant === "trigon_3") + isDownward = ! isDownward + return (isDownward ? 1 : 2) / 3 * gridHeight + } + + Image { + id: image + + width: { + if (isTrigon) return gridWidth * (columns + 1) + else if (isNexos) return gridWidth * (columns - 0.5) + else return gridWidth * columns + } + height: { + if (isNexos) return gridHeight * (rows - 0.5) + else return gridHeight * rows + } + anchors.centerIn: root + source: { + switch (gameVariant) { + case "trigon": + case "trigon_2": + return theme.getImage("board-trigon") + case "trigon_3": + return theme.getImage("board-trigon-3") + case "nexos": + case "nexos_2": + return theme.getImage("board-tile-nexos") + case "callisto": + return theme.getImage("board-callisto") + case "callisto_2": + return theme.getImage("board-callisto-2") + case "callisto_3": + return theme.getImage("board-callisto-3") + default: + return theme.getImage("board-tile-classic") + } + } + sourceSize { + width: { + if (isTrigon || isCallisto) return width + if (isNexos) return 2 * gridWidth + return gridWidth + } + height: { + if (isTrigon || isCallisto) return height + if (isNexos) return 2 * gridHeight + return gridHeight + } + } + // It should work to use Image.Tile for all game variants, but the + // Trigon board is not painted with Image.width/height even if + // sourceSize is bound to it (the Trigon SVG files have a different + // aspect ratio but that shouldn't matter). Bug in Qt 5.6? + fillMode: isTrigon? Image.Stretch : Image.Tile + horizontalAlignment: Image.AlignLeft + verticalAlignment: Image.AlignTop + cache: false + } + Repeater { + model: gameModel.startingPoints0 + + Rectangle { + color: theme.colorBlue + width: startingPointSize; height: width + radius: width / 2 + x: mapFromGameX(modelData.x) + (gridWidth - width) / 2 + y: mapFromGameY(modelData.y) + (gridHeight - height) / 2 + } + } + Repeater { + model: gameModel.startingPoints1 + + Rectangle { + color: gameModel.gameVariant == "duo" + || gameModel.gameVariant == "junior" + || gameModel.gameVariant == "callisto_2" ? + theme.colorGreen : theme.colorYellow + width: startingPointSize; height: width + radius: width / 2 + x: mapFromGameX(modelData.x) + (gridWidth - width) / 2 + y: mapFromGameY(modelData.y) + (gridHeight - height) / 2 + } + } + Repeater { + model: gameModel.startingPoints2 + + Rectangle { + color: theme.colorRed + width: startingPointSize; height: width + radius: width / 2 + x: mapFromGameX(modelData.x) + (gridWidth - width) / 2 + y: mapFromGameY(modelData.y) + (gridHeight - height) / 2 + } + } + Repeater { + model: gameModel.startingPoints3 + + Rectangle { + color: theme.colorGreen + width: startingPointSize; height: width + radius: width / 2 + x: mapFromGameX(modelData.x) + (gridWidth - width) / 2 + y: mapFromGameY(modelData.y) + (gridHeight - height) / 2 + } + } + Repeater { + model: gameModel.startingPointsAll + + Rectangle { + color: theme.colorStartingPoint + width: startingPointSize; height: width + radius: width / 2 + x: mapFromGameX(modelData.x) + (gridWidth - width) / 2 + y: mapFromGameY(modelData.y) + getCenterYTrigon(modelData) + - height / 2 + } + } +} diff --git a/src/pentobi_qml/qml/Button.qml b/src/pentobi_qml/qml/Button.qml new file mode 100644 index 0000000..172c5b0 --- /dev/null +++ b/src/pentobi_qml/qml/Button.qml @@ -0,0 +1,29 @@ +import QtQuick 2.0 +import QtQuick.Window 2.0 +import Qt.labs.controls 1.0 as Controls2 + +/** Button that supports an automatically scaled image. + The image source should be a SVG file with size 22x22. */ +Controls2.Button { + id: root + + property string imageSource + + label: Image { + sourceSize { + // We currently use 22x22 SVG files, try to use 22x22 or 44x44, unless + // very high DPI + width: Screen.pixelDensity < 5 ? 22 : Screen.pixelDensity < 10 ? 44 : 5 * Screen.pixelDensity + height: Screen.pixelDensity < 5 ? 22 : Screen.pixelDensity < 10 ? 44 : 5 * Screen.pixelDensity + } + fillMode: Image.PreserveAspectFit + source: imageSource + opacity: root.enabled ? 1 : 0.4 + cache: false + } + background: Rectangle { + anchors.fill: root + visible: pressed + color: theme.backgroundButtonPressed + } +} diff --git a/src/pentobi_qml/qml/ComputerColorDialog.qml b/src/pentobi_qml/qml/ComputerColorDialog.qml new file mode 100644 index 0000000..db18fcd --- /dev/null +++ b/src/pentobi_qml/qml/ComputerColorDialog.qml @@ -0,0 +1,82 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.1 +import QtQuick.Dialogs 1.2 + +Dialog { + property string gameVariant + property alias computerPlays0: checkBox0.checked + property alias computerPlays1: checkBox1.checked + property alias computerPlays2: checkBox2.checked + property alias computerPlays3: checkBox3.checked + + title: qsTr("Computer Colors") + standardButtons: StandardButton.Ok | StandardButton.Cancel + + GroupBox { + title: qsTr("Computer plays:") + flat: true + + Column { + CheckBox { + id: checkBox0 + + text: { + switch (gameVariant) { + case "classic_2": + case "trigon_2": + case "nexos_2": + return qsTr("Blue/Red") + default: + qsTr("Blue") + } + } + onClicked: { + if (gameVariant == "classic_2" || gameVariant == "trigon_2" + || gameVariant == "nexos_2") + computerPlays2 = checked + } + } + CheckBox { + id: checkBox1 + + text: { + switch (gameVariant) { + case "classic_2": + case "trigon_2": + case "nexos_2": + return qsTr("Yellow/Green") + case "duo": + case "junior": + case "callisto_2": + return qsTr("Green") + default: + qsTr("Yellow") + } + } + onClicked: { + if (gameVariant == "classic_2" || gameVariant == "trigon_2" + || gameVariant == "nexos_2") + computerPlays3 = checked + } + } + CheckBox { + id: checkBox2 + + text: qsTr("Red") + visible: gameVariant == "classic" || gameVariant == "trigon" + || gameVariant == "trigon_3" + || gameVariant == "classic_3" + || gameVariant == "nexos" + || gameVariant == "callisto_3" + || gameVariant == "callisto" + } + CheckBox { + id: checkBox3 + + text: qsTr("Green") + visible: gameVariant == "classic" || gameVariant == "trigon" + || gameVariant == "nexos" || gameVariant == "callisto" + } + } + } +} diff --git a/src/pentobi_qml/qml/GameDisplay.js b/src/pentobi_qml/qml/GameDisplay.js new file mode 100644 index 0000000..58e3d0f --- /dev/null +++ b/src/pentobi_qml/qml/GameDisplay.js @@ -0,0 +1,108 @@ +function createColorPieces(component, pieceModels) { + if (pieceModels.length === 0) + return [] + var colorName + switch (pieceModels[0].color) { + case 0: colorName = "blue"; break + case 1: + colorName = gameModel.gameVariant == "duo" + || gameModel.gameVariant == "junior" + || gameModel.gameVariant == "callisto_2" ? + "green" : "yellow"; break + case 2: colorName = "red"; break + case 3: colorName = "green"; break + } + var properties = { + "colorName": colorName, + "isPicked": Qt.binding(function() { return this === pickedPiece }), + "isMarked": Qt.binding(function() { + return markLastMove && this.pieceModel.isLastMove }) + } + var pieces = [] + for (var i = 0; i < pieceModels.length; ++i) { + properties["pieceModel"] = pieceModels[i] + pieces.push(component.createObject(gameDisplay, properties)) + } + return pieces +} + +function createPieces() { + var file + if (gameModel.gameVariant.indexOf("trigon") === 0) + file = "PieceTrigon.qml" + else if (gameModel.gameVariant.indexOf("nexos") === 0) + file = "PieceNexos.qml" + else if (gameModel.gameVariant.indexOf("callisto") === 0) + file = "PieceCallisto.qml" + else + file = "PieceClassic.qml" + var component = Qt.createComponent(file) + pieces0 = createColorPieces(component, gameModel.pieceModels0) + pieces1 = createColorPieces(component, gameModel.pieceModels1) + pieces2 = createColorPieces(component, gameModel.pieceModels2) + pieces3 = createColorPieces(component, gameModel.pieceModels3) + pieceSelector.transitionsEnabled = + Qt.binding(function() { return enableAnimations }) +} + +function destroyColorPieces(pieces) { + if (pieces === undefined) + return + for (var i = 0; i < pieces.length; ++i) { + pieces[i].visible = false + pieces[i].destroy(1000) + } +} + +function destroyPieces() { + pieceSelector.transitionsEnabled = false + pickedPiece = null + destroyColorPieces(pieces0); pieces0 = [] + destroyColorPieces(pieces1); pieces1 = [] + destroyColorPieces(pieces2); pieces2 = [] + destroyColorPieces(pieces3); pieces3 = [] +} + +function findPiece(pieceModel, color) { + var pieces + switch (color) { + case 0: pieces = pieces0; break + case 1: pieces = pieces1; break + case 2: pieces = pieces2; break + case 3: pieces = pieces3; break + } + if (pieces === undefined) + return null // Pieces haven't been created yet + for (var i = 0; i < pieces.length; ++i) + if (pieces[i].pieceModel === pieceModel) + return pieces[i] + return null +} + +function pickPiece(piece) { + if (playerModel.isGenMoveRunning || gameModel.isGameOver + || piece.pieceModel.color !== gameModel.toPlay) + return + if (! pieceManipulator.visible) { + // Position pieceManipulator at center of piece if possible, but + // make sure it is completely visible + var newCoord = mapFromItem(piece, 0, 0) + var x = newCoord.x - pieceManipulator.width / 2 + var y = newCoord.y - pieceManipulator.height / 2 + x = Math.max(Math.min(x, width - pieceManipulator.width), 0) + y = Math.max(Math.min(y, height - pieceManipulator.height), 0) + pieceManipulator.x = x + pieceManipulator.y = y + } + pickedPiece = piece +} + +function showMoveHint(move) { + var pieceModel = gameModel.preparePiece(gameModel.toPlay, move) + var pos = board.mapToItem(pieceManipulator.parent, + board.mapFromGameX(pieceModel.gameCoord.x), + board.mapFromGameY(pieceModel.gameCoord.y)) + pieceManipulator.x = pos.x - pieceManipulator.width / 2 + pieceManipulator.y = pos.y - pieceManipulator.height / 2 + pickedPiece = findPiece(pieceModel, gameModel.toPlay) +} diff --git a/src/pentobi_qml/qml/GameDisplay.qml b/src/pentobi_qml/qml/GameDisplay.qml new file mode 100644 index 0000000..412dd68 --- /dev/null +++ b/src/pentobi_qml/qml/GameDisplay.qml @@ -0,0 +1,157 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.1 +import "GameDisplay.js" as Logic + +Item +{ + id: gameDisplay // Referenced by Piece*.qml + + property var pickedPiece: null + property bool markLastMove: true + property bool enableAnimations: true + property alias busyIndicatorRunning: busyIndicator.running + property size imageSourceSize: { + var width = board.gridWidth, height = board.gridHeight + if (board.isTrigon) + return Qt.size(2 * width, height) + if (board.isNexos) + return Qt.size(1.5 * width, 1.5 * height) + if (board.isCallisto) + return Qt.size(0.9 * width, 0.9 * height) + return Qt.size(width, height) + } + property alias pieces0: pieceSelector.pieces0 + property alias pieces1: pieceSelector.pieces1 + property alias pieces2: pieceSelector.pieces2 + property alias pieces3: pieceSelector.pieces3 + + signal play(var pieceModel, point gameCoord) + + function createPieces() { Logic.createPieces() } + function destroyPieces() { Logic.destroyPieces() } + function showToPlay() { pieceSelector.contentY = 0 } + function showMoveHint(move) { Logic.showMoveHint(move) } + + onWidthChanged: pickedPiece = null + onHeightChanged: pickedPiece = null + + Column { + id: column + + width: gameDisplay.width + anchors.centerIn: gameDisplay + spacing: 0.01 * board.width + + Board { + id: board + + gameVariant: gameModel.gameVariant + width: Math.min( + parent.width, + gameDisplay.height / (1.07 + 2.7 / pieceSelector.columns)) + height: isTrigon ? Math.sqrt(3) / 2 * width : width + anchors.horizontalCenter: parent.horizontalCenter + } + ScoreDisplay { + id: scoreDisplay + + gameVariant: gameModel.gameVariant + points0: gameModel.points0 + points1: gameModel.points1 + points2: gameModel.points2 + points3: gameModel.points3 + bonus0: gameModel.bonus0 + bonus1: gameModel.bonus1 + bonus2: gameModel.bonus2 + bonus3: gameModel.bonus3 + hasMoves0: gameModel.hasMoves0 + hasMoves1: gameModel.hasMoves1 + hasMoves2: gameModel.hasMoves2 + hasMoves3: gameModel.hasMoves3 + toPlay: gameModel.isGameOver ? -1 : gameModel.toPlay + altPlayer: gameModel.altPlayer + height: board.width / 20 + pointSize: 0.6 * height + anchors.horizontalCenter: parent.horizontalCenter + } + Flickable { + id: flickable + + width: 0.9 * board.width + height: width / pieceSelector.columns * pieceSelector.rows + contentWidth: 2 * width + contentHeight: height + anchors.horizontalCenter: board.horizontalCenter + clip: true + onMovementEnded: { + snapAnimation.to = contentX > width / 2 ? width : 0 + snapAnimation.restart() + } + + Row { + id: flickableContent + + PieceSelector { + id: pieceSelector + + columns: gameModel.gameVariant.indexOf("classic") == 0 + || gameModel.gameVariant.indexOf("callisto") == 0 + || gameModel.gameVariant == "duo" ? 7 : 8 + width: flickable.width + height: flickable.height + rows: 3 + gameVariant: gameModel.gameVariant + toPlay: gameModel.toPlay + nuColors: gameModel.nuColors + transitionsEnabled: false + onPiecePicked: Logic.pickPiece(piece) + } + NavigationPanel { + width: flickable.width + height: flickable.height + } + } + SmoothedAnimation { + id: snapAnimation + + target: flickable + property: "contentX" + duration: 200 + } + } + } + BusyIndicator { + id: busyIndicator + + x: (gameDisplay.width - width) / 2 + y: column.y + flickable.y + (flickable.height - height) / 2 + } + PieceManipulator { + id: pieceManipulator + + legal: { + if (pickedPiece === null) + return false + // Don't use mapToItem(board, width / 2, height / 2), we want a + // dependency on x, y. + var pos = parent.mapToItem(board, x + width / 2, y + height / 2) + return gameModel.isLegalPos(pickedPiece.pieceModel, + pickedPiece.pieceModel.state, + board.mapToGame(pos)) + } + width: 0.6 * board.width; height: width + visible: pickedPiece !== null + pieceModel: pickedPiece !== null ? pickedPiece.pieceModel : null + onPiecePlayed: { + var pos = mapToItem(board, width / 2, height / 2) + if (! board.contains(Qt.point(pos.x, pos.y))) + pickedPiece = null + else if (legal) + play(pieceModel, board.mapToGame(pos)) + } + } + Connections { + target: gameModel + onPositionChanged: pickedPiece = null + } +} diff --git a/src/pentobi_qml/qml/LineSegment.qml b/src/pentobi_qml/qml/LineSegment.qml new file mode 100644 index 0000000..b309853 --- /dev/null +++ b/src/pentobi_qml/qml/LineSegment.qml @@ -0,0 +1,105 @@ +import QtQuick 2.3 + +// Piece element for Nexos. See Square.qml for comments. +Item { + id: root + + property bool isHorizontal + + Loader { + function loadImage() { + if (opacity > 0 && status === Loader.Null) + sourceComponent = component0 + } + + anchors.fill: root + opacity: imageOpacity0 + onOpacityChanged: loadImage() + Component.onCompleted: loadImage() + + Component { + id: component0 + + Image { + source: imageName + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + mirror: ! isHorizontal + rotation: isHorizontal ? 0 : -90 + } + } + } + Loader { + function loadImage() { + if (opacity > 0 && status === Loader.Null) + sourceComponent = component90 + } + + anchors.fill: root + opacity: imageOpacity90 + onOpacityChanged: loadImage() + Component.onCompleted: loadImage() + + Component { + id: component90 + + Image { + source: imageName + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + mirror: isHorizontal + rotation: isHorizontal ? -180 : -90 + } + } + } + Loader { + function loadImage() { + if (opacity > 0 && status === Loader.Null) + sourceComponent = component180 + } + + anchors.fill: root + opacity: imageOpacity180 + onOpacityChanged: loadImage() + Component.onCompleted: loadImage() + + Component { + id: component180 + + Image { + source: imageName + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + mirror: ! isHorizontal + rotation: isHorizontal ? -180 : -270 + } + } + } + Loader { + function loadImage() { + if (opacity > 0 && status === Loader.Null) + sourceComponent = component270 + } + + anchors.fill: root + opacity: imageOpacity270 + onOpacityChanged: loadImage() + Component.onCompleted: loadImage() + + Component { + id: component270 + + Image { + source: imageName + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + mirror: isHorizontal + rotation: isHorizontal ? 0 : -270 + } + } + } +} diff --git a/src/pentobi_qml/qml/Main.js b/src/pentobi_qml/qml/Main.js new file mode 100644 index 0000000..d6a3963 --- /dev/null +++ b/src/pentobi_qml/qml/Main.js @@ -0,0 +1,274 @@ +function about() { + var url = "http://pentobi.sourceforge.net" + showInfo("

" + qsTr("Pentobi") + "

" + + qsTr("Version %1").arg(Qt.application.version) + "

" + + qsTr("Computer opponent for the board game Blokus.") + "
" + + qsTr("© 2011–%1 Markus Enzenberger").arg(2017) + + "
" + url + "

") +} + +function changeGameVariant(gameVariant) { + if (gameModel.gameVariant === gameVariant) + return + if (! gameModel.isGameEmpty && ! gameModel.isGameOver) { + showQuestion(qsTr("New game?"), + function() { changeGameVariantNoVerify(gameVariant) }) + return + } + changeGameVariantNoVerify(gameVariant) +} + +function changeGameVariantNoVerify(gameVariant) { + cancelGenMove() + lengthyCommand.run(function() { + gameDisplay.destroyPieces() + gameModel.initGameVariant(gameVariant) + gameDisplay.createPieces() + gameDisplay.showToPlay() + initComputerColors() + }) +} + +function checkComputerMove() { + if (gameModel.isGameOver) { + showInfo(gameModel.getResultMessage()) + return + } + if (! isComputerToPlay()) + return + switch (gameModel.toPlay) { + case 0: if (! gameModel.hasMoves0) return; break + case 1: if (! gameModel.hasMoves1) return; break + case 2: if (! gameModel.hasMoves2) return; break + case 3: if (! gameModel.hasMoves3) return; break + } + genMove(); +} + +/** If the computer already plays the current color to play, start generating + a move; if he doesn't, make him play the current color (and only the + current color). */ +function computerPlay() { + if (playerModel.isGenMoveRunning) + return + if (! isComputerToPlay()) { + computerPlays0 = false + computerPlays1 = false + computerPlays2 = false + computerPlays3 = false + var variant = gameModel.gameVariant + if (variant == "classic_3" && gameModel.toPlay == 3) { + switch (gameModel.altPlayer) { + case 0: computerPlays0 = true; break + case 1: computerPlays1 = true; break + case 2: computerPlays2 = true; break + } + } + else + { + var isMultiColor = + (variant == "classic_2" || variant == "trigon_2" + || variant == "nexos_2") + switch (gameModel.toPlay) { + case 0: + computerPlays0 = true + if (isMultiColor) computerPlays2 = true + break; + case 1: + computerPlays1 = true + if (isMultiColor) computerPlays3 = true + break; + case 2: + computerPlays2 = true + if (isMultiColor) computerPlays0 = true + break; + case 3: + computerPlays3 = true + if (isMultiColor) computerPlays1 = true + break; + } + } + } + checkComputerMove() +} + +function computerPlays(color) { + switch (color) { + case 0: return computerPlays0 + case 1: return computerPlays1 + case 2: return computerPlays2 + case 3: return computerPlays3 + } +} + +function createTheme(themeName) { + var source = "qrc:///qml/themes/" + themeName + "/Theme.qml" + return Qt.createComponent(source).createObject(root) +} + +function deleteAllVar() { + showQuestion(qsTr("Delete all variations?"), gameModel.deleteAllVar) +} + +function genMove() { + gameDisplay.pickedPiece = null + isMoveHintRunning = false + playerModel.startGenMove(gameModel) +} + +function getFileFromUrl(fileUrl) { + var file = fileUrl.toString() + file = file.replace(/^(file:\/{3})/,"/") + return decodeURIComponent(file) +} + +function init() { + // Settings might contain unusable geometry + var maxWidth = Screen.desktopAvailableWidth + var maxHeight = Screen.desktopAvailableHeight + if (x < 0 || x + width > maxWidth || y < 0 || y + height > maxHeight) { + if (width > maxWidth || height > Screen.maxHeight) { + width = defaultWidth + height = defaultHeight + } + x = (maxWidth - width) / 2 + y = (maxHeight - height) / 2 + } + if (! gameModel.loadAutoSave()) { + gameDisplay.createPieces() + initComputerColors() + } + else { + gameDisplay.createPieces() + if (! gameModel.isGameOver) + checkComputerMove() + } +} + +function initComputerColors() { + // Default setting is that the computer plays all colors but the first + computerPlays0 = false + computerPlays1 = true + computerPlays2 = true + computerPlays3 = true + if (gameModel.gameVariant == "classic_2" + || gameModel.gameVariant == "trigon_2" + || gameModel.gameVariant == "nexos_2") + computerPlays2 = false +} + +function isComputerToPlay() { + if (gameModel.gameVariant == "classic_3" && gameModel.toPlay == 3) + return computerPlays(gameModel.altPlayer) + return computerPlays(gameModel.toPlay) +} + +function moveGenerated(move) { + if (isMoveHintRunning) { + gameDisplay.showMoveHint(move) + isMoveHintRunning = false + return + } + gameModel.playMove(move) + delayedCheckComputerMove.restart() +} + +function moveHint() { + if (gameModel.isGameOver) + return + isMoveHintRunning = true + playerModel.startGenMoveAtLevel(gameModel, 1) +} + +function newGameNoVerify() +{ + gameModel.newGame() + gameDisplay.showToPlay() + initComputerColors() +} + +function newGame() +{ + if (! gameModel.isGameEmpty && ! gameModel.isGameOver) { + showQuestion(qsTr("New game?"), newGameNoVerify) + return + } + newGameNoVerify() +} + +function openFileUrl() { + gameDisplay.destroyPieces() + if (! gameModel.open(getFileFromUrl(openDialog.item.fileUrl))) + showError(qsTr("Open failed.") + "\n" + gameModel.lastInputOutputError) + else { + computerPlays0 = false + computerPlays1 = false + computerPlays2 = false + computerPlays3 = false + } + gameDisplay.createPieces() + gameDisplay.showToPlay() +} + +function play(pieceModel, gameCoord) { + var wasComputerToPlay = isComputerToPlay() + gameModel.playPiece(pieceModel, gameCoord) + // We don't continue automatic play if the human played a move for a color + // played by the computer. + if (! wasComputerToPlay) + delayedCheckComputerMove.restart() +} + +function saveFileUrl(fileUrl) { + if (! gameModel.save(getFileFromUrl(fileUrl))) + showError(qsTr("Save failed.") + "\n" + gameModel.lastInputOutputError) +} + +function showComputerColorDialog() { + if (computerColorDialogLoader.status === Loader.Null) + computerColorDialogLoader.sourceComponent = + computerColorDialogComponent + var dialog = computerColorDialogLoader.item + dialog.computerPlays0 = computerPlays0 + dialog.computerPlays1 = computerPlays1 + dialog.computerPlays2 = computerPlays2 + dialog.computerPlays3 = computerPlays3 + dialog.open() +} + +function showError(text) { + if (errorMessageLoader.status === Loader.Null) + errorMessageLoader.sourceComponent = errorMessageComponent + var dialog = errorMessageLoader.item + dialog.text = text + dialog.open() +} + +function showInfo(text) { + if (infoMessageLoader.status === Loader.Null) + infoMessageLoader.sourceComponent = infoMessageComponent + var dialog = infoMessageLoader.item + dialog.text = text + dialog.open() +} + +function showQuestion(text, acceptedFunc) { + if (questionMessageLoader.status === Loader.Null) + questionMessageLoader.sourceComponent = questionMessageComponent + var dialog = questionMessageLoader.item + dialog.text = text + dialog.accepted.connect(acceptedFunc) + dialog.open() +} + +function truncate() { + showQuestion(qsTr("Truncate this subtree?"), gameModel.truncate) +} + +function truncateChildren() { + showQuestion(qsTr("Truncate children?"), gameModel.truncateChildren) +} + +function undo() { + gameModel.undo() +} diff --git a/src/pentobi_qml/qml/Main.qml b/src/pentobi_qml/qml/Main.qml new file mode 100644 index 0000000..a90ba50 --- /dev/null +++ b/src/pentobi_qml/qml/Main.qml @@ -0,0 +1,234 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.1 +import QtQuick.Dialogs 1.2 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.1 +import Qt.labs.settings 1.0 +import pentobi 1.0 +import "." as Pentobi +import "Main.js" as Logic + +ApplicationWindow { + id: root + + property bool computerPlays0 + property bool computerPlays1 + property bool computerPlays2 + property bool computerPlays3 + property bool isMoveHintRunning + property bool isAndroid: Qt.platform.os === "android" + property string themeName: isAndroid ? "dark" : "light" + property QtObject theme: Logic.createTheme(themeName) + property url folder + property int defaultWidth: + isAndroid ? Screen.desktopAvailableWidth : + Math.min(Screen.desktopAvailableWidth, + Math.round(Screen.pixelDensity / 3.5 * 600)) + property int defaultHeight: + isAndroid ? Screen.desktopAvailableWidth : + Math.min(Math.round(Screen.pixelDensity / 3.5 * 800)) + + function cancelGenMove() { + playerModel.cancelGenMove() + delayedCheckComputerMove.stop() + } + + minimumWidth: 240; minimumHeight: 320 + width: isAndroid ? Screen.desktopAvailableWidth : defaultWidth + height: isAndroid ? Screen.desktopAvailableHeight : defaultHeight + color: theme.backgroundColor + title: qsTr("Pentobi") + onClosing: Qt.quit() + // Currently, we don't use the QtQuick ToolBar/MenuBar on Android. The file + // dialog is unusable with dark themes (QTBUG-48324) and a white toolbar is + // too distracting with the dark background we use on Android. + menuBar: menuBarLoader.item + toolBar: toolBarLoader.item + Component.onCompleted: { + Logic.init() + show() + } + Component.onDestruction: gameModel.autoSave() + + ColumnLayout { + anchors.fill: parent + Keys.onReleased: if (isAndroid && event.key === Qt.Key_Menu) { + androidToolBarLoader.item.popupMenu() + event.accepted = true + } + + Loader { + id: androidToolBarLoader + + sourceComponent: isAndroid ? androidToolBarComponent : undefined + Layout.fillWidth: true + + Component { + id: androidToolBarComponent + + AndroidToolBar { } + } + } + GameDisplay { + id: gameDisplay + + busyIndicatorRunning: pieces0 === undefined + || lengthyCommand.isRunning + || playerModel.isGenMoveRunning + Layout.fillWidth: true + Layout.fillHeight: true + focus: true + onPlay: Logic.play(pieceModel, gameCoord) + } + } + Loader { + id: menuBarLoader + + sourceComponent: isAndroid ? undefined : menuBarComponent + + Component { + id: menuBarComponent + + MenuBar { + MenuGame { } + MenuGo { } + MenuEdit { } + MenuComputer { } + MenuView { } + MenuHelp { } + } + } + } + Loader { + id: toolBarLoader + + sourceComponent: isAndroid ? undefined : toolBarComponent + + Component { + id: toolBarComponent + + Pentobi.ToolBar { } + } + } + Settings { + id: settings + + property alias x: root.x + property alias y: root.y + property alias width: root.width + property alias height: root.height + property alias folder: root.folder + property alias enableAnimations: gameDisplay.enableAnimations + property alias markLastMove: gameDisplay.markLastMove + property alias computerPlays0: root.computerPlays0 + property alias computerPlays1: root.computerPlays1 + property alias computerPlays2: root.computerPlays2 + property alias computerPlays3: root.computerPlays3 + } + GameModel { + id: gameModel + + onPositionAboutToChange: cancelGenMove() + } + PlayerModel { + id: playerModel + + onMoveGenerated: Logic.moveGenerated(move) + } + Loader { id: computerColorDialogLoader } + Component { + id: computerColorDialogComponent + + ComputerColorDialog { + id: computerColorDialog + + gameVariant: gameModel.gameVariant + onAccepted: { + root.computerPlays0 = computerColorDialog.computerPlays0 + root.computerPlays1 = computerColorDialog.computerPlays1 + root.computerPlays2 = computerColorDialog.computerPlays2 + root.computerPlays3 = computerColorDialog.computerPlays3 + if (! Logic.isComputerToPlay()) + cancelGenMove() + else if (! gameModel.isGameOver) + Logic.checkComputerMove() + gameDisplay.forceActiveFocus() // QTBUG-48456 + } + onRejected: gameDisplay.forceActiveFocus() // QTBUG-48456 + } + } + Loader { + id: openDialog + + function open() { + if (status === Loader.Null) + setSource("OpenDialog.qml") + item.open() + } + } + Loader { + id: saveDialog + + function open() { + if (status === Loader.Null) + source = "SaveDialog.qml" + item.open() + } + } + Loader { id: errorMessageLoader } + Component { + id: errorMessageComponent + + MessageDialog { + icon: StandardIcon.Critical + } + } + Loader { id: infoMessageLoader } + Component { + id: infoMessageComponent + + MessageDialog { } + } + Loader { id: questionMessageLoader } + Component { + id: questionMessageComponent + + MessageDialog { + standardButtons: StandardButton.Ok | StandardButton.Cancel + } + } + // Used to delay calls to Logic.checkComputerMove such that the computer + // starts thinking and the busy indicator is visible after the current move + // placement animation has finished + Timer { + id: delayedCheckComputerMove + + interval: 400 + onTriggered: Logic.checkComputerMove() + } + // Delay lengthy function calls such that the busy indicator is visible + Timer { + id: lengthyCommand + + property bool isRunning + property var func + + function run(func) { + lengthyCommand.func = func + isRunning = true + restart() + } + + interval: 400 + onTriggered: { + func() + isRunning = false + } + } + Connections { + target: Qt.application + onStateChanged: + if (Qt.application.state === Qt.ApplicationSuspended) + gameModel.autoSave() + } +} diff --git a/src/pentobi_qml/qml/MenuComputer.qml b/src/pentobi_qml/qml/MenuComputer.qml new file mode 100644 index 0000000..6299b2b --- /dev/null +++ b/src/pentobi_qml/qml/MenuComputer.qml @@ -0,0 +1,47 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.1 +import "Main.js" as Logic + +Menu { + title: qsTr("&Computer") + + MenuItem { + text: qsTr("Computer &Colors") + visible: ! isAndroid + onTriggered: Logic.showComputerColorDialog() + } + MenuItem { + text: qsTr("&Play") + enabled: ! gameModel.isGameOver + visible: ! isAndroid + onTriggered: Logic.computerPlay() + } + Menu { + title: + switch (gameModel.gameVariant) + { + case "classic": return qsTr("&Level (Classic, 4 Players)") + case "classic_2": return qsTr("&Level (Classic, 2 Players)") + case "classic_3": return qsTr("&Level (Classic, 3 Players)") + case "duo": return qsTr("&Level (Duo)") + case "junior": return qsTr("&Level (Junior)") + case "trigon": return qsTr("&Level (Trigon, 4 Players)") + case "trigon_2": return qsTr("&Level (Trigon, 2 Players)") + case "trigon_3": return qsTr("&Level (Trigon, 3 Players)") + case "nexos": return qsTr("&Level (Nexos, 4 Players)") + case "nexos_2": return qsTr("&Level (Nexos, 2 Players)") + case "callisto": return qsTr("&Level (Callisto, 4 Players)") + case "callisto_2": return qsTr("&Level (Callisto, 2 Players)") + case "callisto_3": return qsTr("&Level (Callisto, 3 Players)") + } + + ExclusiveGroup { id: levelGroup } + MenuItemLevel { level: 1 } + MenuItemLevel { level: 2 } + MenuItemLevel { level: 3 } + MenuItemLevel { level: 4 } + MenuItemLevel { level: 5 } + MenuItemLevel { level: 6 } + MenuItemLevel { level: 7 } + } +} diff --git a/src/pentobi_qml/qml/MenuEdit.qml b/src/pentobi_qml/qml/MenuEdit.qml new file mode 100644 index 0000000..cacfaa1 --- /dev/null +++ b/src/pentobi_qml/qml/MenuEdit.qml @@ -0,0 +1,50 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.1 +import "Main.js" as Logic + +Menu { + title: qsTr("&Edit") + + MenuItem { + text: qsTr("Make &Main Variation") + enabled: ! gameModel.isMainVar + visible: ! isAndroid || enabled + onTriggered: gameModel.makeMainVar() + } + MenuItem { + text: qsTr("Move Variation &Up") + enabled: gameModel.hasPrevVar + visible: ! isAndroid || enabled + onTriggered: gameModel.moveUpVar() + } + MenuItem { + text: qsTr("Move Variation &Down") + enabled: gameModel.hasNextVar + visible: ! isAndroid || enabled + onTriggered: gameModel.moveDownVar() + } + MenuSeparator { } + MenuItem { + text: qsTr("&Delete All Variations") + enabled: gameModel.hasVariations + visible: ! isAndroid || enabled + onTriggered: Logic.deleteAllVar() + } + MenuItem { + text: qsTr("&Truncate") + enabled: gameModel.canGoBackward + visible: ! isAndroid || enabled + onTriggered: Logic.truncate() + } + MenuItem { + text: qsTr("Truncate &Children") + enabled: gameModel.canGoForward + visible: ! isAndroid || enabled + onTriggered: Logic.truncateChildren() + } + MenuSeparator { } + MenuItem { + text: qsTr("&Next Color") + onTriggered: gameModel.nextColor() + } +} diff --git a/src/pentobi_qml/qml/MenuGame.qml b/src/pentobi_qml/qml/MenuGame.qml new file mode 100644 index 0000000..7ebd92e --- /dev/null +++ b/src/pentobi_qml/qml/MenuGame.qml @@ -0,0 +1,119 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.1 +import "Main.js" as Logic + +Menu { + title: qsTr("&Game") + + MenuItem { + text: qsTr("&New") + enabled: ! gameModel.isGameEmpty + visible: ! isAndroid + onTriggered: Logic.newGame() + } + MenuSeparator { + visible: ! isAndroid + } + Menu { + title: qsTr("Game &Variant") + + ExclusiveGroup { id: groupGameVariant } + Menu { + title: qsTr("&Classic") + + MenuItemGameVariant { + gameVariant: "classic_2" + text: qsTr("Classic (&2 Players)") + } + MenuItemGameVariant { + gameVariant: "classic_3" + text: qsTr("Classic (&3 Players)") + } + MenuItemGameVariant { + gameVariant: "classic" + text: qsTr("Classic (&4 Players)") + } + } + MenuItemGameVariant { + gameVariant: "duo" + text: qsTr("&Duo") + } + MenuItemGameVariant { + gameVariant: "junior" + text: qsTr("&Junior") + } + Menu { + title: qsTr("&Trigon") + + MenuItemGameVariant { + gameVariant: "trigon_2" + text: qsTr("Trigon (&2 Players)") + } + MenuItemGameVariant { + gameVariant: "trigon_3" + text: qsTr("Trigon (&3 Players)") + } + MenuItemGameVariant { + gameVariant: "trigon" + text: qsTr("Trigon (&4 Players)") + } + } + Menu { + title: qsTr("&Nexos") + + MenuItemGameVariant { + gameVariant: "nexos_2" + text: qsTr("Nexos (&2 Players)") + } + MenuItemGameVariant { + gameVariant: "nexos" + text: qsTr("Nexos (&4 Players)") + } + } + Menu { + title: qsTr("C&allisto") + + MenuItemGameVariant { + gameVariant: "callisto_2" + text: qsTr("Callisto (&2 Players)") + } + MenuItemGameVariant { + gameVariant: "callisto_3" + text: qsTr("Callisto (&3 Players)") + } + MenuItemGameVariant { + gameVariant: "callisto" + text: qsTr("Callisto (&4 Players)") + } + } + } + MenuSeparator { } + MenuItem { + text: qsTr("&Undo Move") + enabled: gameModel.canUndo + visible: ! isAndroid + onTriggered: Logic.undo() + } + MenuItem { + text: qsTr("&Find Move") + enabled: ! gameModel.isGameOver + visible: ! isAndroid || enabled + onTriggered: Logic.moveHint() + } + MenuSeparator { } + MenuItem { + text: qsTr("&Open...") + onTriggered: openDialog.open() + } + MenuItem { + text: qsTr("&Save As...") + enabled: ! gameModel.isGameEmpty + visible: ! isAndroid || enabled + onTriggered: saveDialog.open() + } + MenuSeparator { } + MenuItem { + text: qsTr("&Quit") + onTriggered: Qt.quit() + } +} diff --git a/src/pentobi_qml/qml/MenuGo.qml b/src/pentobi_qml/qml/MenuGo.qml new file mode 100644 index 0000000..253ff0f --- /dev/null +++ b/src/pentobi_qml/qml/MenuGo.qml @@ -0,0 +1,17 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.1 +import "Main.js" as Logic + +Menu { + title: qsTr("G&o") + visible: ! isAndroid || backToMainVar.enabled + + MenuItem { + id: backToMainVar + + text: qsTr("Back to &Main Variation") + enabled: ! gameModel.isMainVar + visible: ! isAndroid || enabled + onTriggered: gameModel.backToMainVar() + } +} diff --git a/src/pentobi_qml/qml/MenuHelp.qml b/src/pentobi_qml/qml/MenuHelp.qml new file mode 100644 index 0000000..d26ea34 --- /dev/null +++ b/src/pentobi_qml/qml/MenuHelp.qml @@ -0,0 +1,12 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.1 +import "Main.js" as Logic + +Menu { + title: qsTr("&Help") + + MenuItem { + text: qsTr("&About Pentobi") + onTriggered: Logic.about() + } +} diff --git a/src/pentobi_qml/qml/MenuItemGameVariant.qml b/src/pentobi_qml/qml/MenuItemGameVariant.qml new file mode 100644 index 0000000..6999084 --- /dev/null +++ b/src/pentobi_qml/qml/MenuItemGameVariant.qml @@ -0,0 +1,12 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.1 +import "Main.js" as Logic + +MenuItem { + property string gameVariant + + checkable: true + checked: gameModel.gameVariant == gameVariant + exclusiveGroup: groupGameVariant + onTriggered: Logic.changeGameVariant(gameVariant) +} diff --git a/src/pentobi_qml/qml/MenuItemLevel.qml b/src/pentobi_qml/qml/MenuItemLevel.qml new file mode 100644 index 0000000..6c3c9c1 --- /dev/null +++ b/src/pentobi_qml/qml/MenuItemLevel.qml @@ -0,0 +1,43 @@ +import QtQuick.Controls 1.1 + +MenuItem { + property int level + + text: "&" + level + checkable: true + exclusiveGroup: levelGroup + checked: { + switch (gameModel.gameVariant) { + case "classic_2": return playerModel.levelClassic2 === level + case "classic_3": return playerModel.levelClassic3 === level + case "duo": return playerModel.levelDuo === level + case "trigon": return playerModel.levelTrigon === level + case "trigon_2": return playerModel.levelTrigon2 === level + case "trigon_3": return playerModel.levelTrigon3 === level + case "junior": return playerModel.levelJunior === level + case "nexos": return playerModel.levelNexos === level + case "nexos_2": return playerModel.levelNexos2 === level + case "callisto": return playerModel.levelCallisto === level + case "callisto_2": return playerModel.levelCallisto2 === level + case "callisto_3": return playerModel.levelCallisto3 === level + default: return playerModel.levelClassic === level + } + } + onTriggered: { + switch (gameModel.gameVariant) { + case "classic_2": playerModel.levelClassic2 = level; break + case "classic_3": playerModel.levelClassic3 = level; break + case "duo": playerModel.levelDuo = level; break + case "trigon": playerModel.levelTrigon = level; break + case "trigon_2": playerModel.levelTrigon2 = level; break + case "trigon_3": playerModel.levelTrigon3 = level; break + case "junior": playerModel.levelJunior = level; break + case "nexos": playerModel.levelNexos = level; break + case "nexos_2": playerModel.levelNexos2 = level; break + case "callisto": playerModel.levelCallisto = level; break + case "callisto_2": playerModel.levelCallisto2 = level; break + case "callisto_3": playerModel.levelCallisto3 = level; break + default: playerModel.levelClassic = level + } + } +} diff --git a/src/pentobi_qml/qml/MenuView.qml b/src/pentobi_qml/qml/MenuView.qml new file mode 100644 index 0000000..cec07f5 --- /dev/null +++ b/src/pentobi_qml/qml/MenuView.qml @@ -0,0 +1,19 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.1 + +Menu { + title: qsTr("&View") + + MenuItem { + text: qsTr("Mark &Last Move") + checkable: true + checked: gameDisplay.markLastMove + onTriggered: gameDisplay.markLastMove = checked + } + MenuItem { + text: qsTr("&Animate Pieces") + checkable: true + checked: gameDisplay.enableAnimations + onTriggered: gameDisplay.enableAnimations = checked + } +} diff --git a/src/pentobi_qml/qml/NavigationPanel.qml b/src/pentobi_qml/qml/NavigationPanel.qml new file mode 100644 index 0000000..4db4d53 --- /dev/null +++ b/src/pentobi_qml/qml/NavigationPanel.qml @@ -0,0 +1,57 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.4 +import QtQuick.Layouts 1.0 +import "." as Pentobi + +ColumnLayout { + id: root + + Text { + text: gameModel.positionInfo + color: theme.fontColorPosInfo + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + } + RowLayout + { + width: root.width; height: width / 6 + + Pentobi.Button { + enabled: gameModel.canGoBackward + imageSource: "icons/pentobi-beginning.svg" + Layout.fillWidth: true + onClicked: gameModel.goBeginning() + } + Pentobi.Button { + enabled: gameModel.canGoBackward + imageSource: "icons/pentobi-backward.svg" + Layout.fillWidth: true + onClicked: gameModel.goBackward() + autoRepeat: true + } + Pentobi.Button { + enabled: gameModel.canGoForward + imageSource: "icons/pentobi-forward.svg" + Layout.fillWidth: true + onClicked: gameModel.goForward() + autoRepeat: true + } + Pentobi.Button { + enabled: gameModel.canGoForward + imageSource: "icons/pentobi-end.svg" + Layout.fillWidth: true + onClicked: gameModel.goEnd() + } + Pentobi.Button { + enabled: gameModel.hasPrevVar + imageSource: "icons/pentobi-previous-variation.svg" + Layout.fillWidth: true + onClicked: gameModel.goPrevVar() + } + Pentobi.Button { + enabled: gameModel.hasNextVar + imageSource: "icons/pentobi-next-variation.svg" + Layout.fillWidth: true + onClicked: gameModel.goNextVar() + } + } +} diff --git a/src/pentobi_qml/qml/OpenDialog.qml b/src/pentobi_qml/qml/OpenDialog.qml new file mode 100644 index 0000000..c9076c7 --- /dev/null +++ b/src/pentobi_qml/qml/OpenDialog.qml @@ -0,0 +1,15 @@ +import QtQuick 2.0 +import QtQuick.Dialogs 1.2 +import "Main.js" as Logic + +FileDialog { + title: qsTr("Open") + nameFilters: [ qsTr("Blokus games (*.blksgf)"), qsTr("All files (*)") ] + folder: root.folder == "" ? shortcuts.desktop : root.folder + onAccepted: { + root.folder = folder + gameDisplay.forceActiveFocus() // QTBUG-48456 + lengthyCommand.run(Logic.openFileUrl) + } + onRejected: gameDisplay.forceActiveFocus() // QTBUG-48456 +} diff --git a/src/pentobi_qml/qml/PieceCallisto.qml b/src/pentobi_qml/qml/PieceCallisto.qml new file mode 100644 index 0000000..9f635d8 --- /dev/null +++ b/src/pentobi_qml/qml/PieceCallisto.qml @@ -0,0 +1,293 @@ +import QtQuick 2.3 + +// See PieceClassic.qml for comments +Item +{ + id: root + + property var pieceModel + property string colorName + property bool isPicked + property Item parentUnplayed + property real gridWidth: board.gridWidth + property real gridHeight: board.gridHeight + property bool isMarked + property string imageName: pieceModel.elements.length === 1 ? + theme.getImage("frame-" + colorName) : + theme.getImage("square-" + colorName) + property real pieceAngle: { + var flX = Math.abs(flipX.angle % 360 - 180) < 90 + var flY = Math.abs(flipY.angle % 360 - 180) < 90 + var angle = rotation + if (flX && flY) angle += 180 + else if (flX) angle += 90 + else if (flY) angle += 270 + return angle + } + property real imageOpacity0: imageOpacity(pieceAngle, 0) + property real imageOpacity90: imageOpacity(pieceAngle, 90) + property real imageOpacity180: imageOpacity(pieceAngle, 180) + property real imageOpacity270: imageOpacity(pieceAngle, 270) + + z: 1 + transform: [ + Rotation { + id: flipX + + axis { x: 1; y: 0; z: 0 } + origin { x: width / 2; y: height / 2 } + }, + Rotation { + id: flipY + + axis { x: 0; y: 1; z: 0 } + origin { x: width / 2; y: height / 2 } + } + ] + + function imageOpacity(pieceAngle, imgAngle) { + var angle = (((pieceAngle - imgAngle) % 360) + 360) % 360 + return (angle >= 90 && angle <= 270 ? 0 : Math.cos(angle * Math.PI / 180)) + } + + Repeater { + model: pieceModel.elements + + Item { + Square { + width: 0.9 * gridWidth + height: 0.9 * gridHeight + x: (modelData.x - pieceModel.center.x) * gridWidth + + (gridWidth - width) / 2 + y: (modelData.y - pieceModel.center.y) * gridHeight + + (gridHeight - height) / 2 + } + // Right junction + Image { + visible: pieceModel.junctionType[index] === 0 + || pieceModel.junctionType[index] === 1 + source: theme.getImage("junction-all-" + colorName) + width: 0.1 * gridWidth + height: 0.85 * gridHeight + x: (modelData.x - pieceModel.center.x + 1) * gridWidth + - width / 2 + y: (modelData.y - pieceModel.center.y) * gridHeight + + (gridHeight - height) / 2 + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + } + // Down junction + Image { + visible: pieceModel.junctionType[index] === 0 + || pieceModel.junctionType[index] === 2 + source: theme.getImage("junction-all-" + colorName) + width: 0.85 * gridWidth + height: 0.1 * gridHeight + x: (modelData.x - pieceModel.center.x) * gridWidth + + (gridWidth - width) / 2 + y: (modelData.y - pieceModel.center.y + 1) * gridHeight + - height / 2 + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + } + } + } + Rectangle { + opacity: isMarked ? 0.5 : 0 + color: colorName == "blue" || colorName == "red" + || pieceModel.elements.length === 1 ? "white" : "#333333" + width: 0.3 * gridHeight + height: width + radius: width / 2 + x: (pieceModel.labelPos.x - pieceModel.center.x + 0.5) + * gridWidth - width / 2 + y: (pieceModel.labelPos.y - pieceModel.center.y + 0.5) + * gridHeight - height / 2 + Behavior on opacity { NumberAnimation { duration: 80 } } + } + StateGroup { + state: pieceModel.state + + states: [ + State { + name: "rot90" + PropertyChanges { target: root; rotation: 90 } + }, + State { + name: "rot180" + PropertyChanges { target: root; rotation: 180 } + }, + State { + name: "rot270" + PropertyChanges { target: root; rotation: 270 } + }, + State { + name: "flip" + PropertyChanges { target: flipX; angle: 180 } + }, + State { + name: "rot90Flip" + PropertyChanges { target: root; rotation: 90 } + PropertyChanges { target: flipX; angle: 180 } + }, + State { + name: "rot180Flip" + PropertyChanges { target: root; rotation: 180 } + PropertyChanges { target: flipX; angle: 180 } + }, + State { + name: "rot270Flip" + PropertyChanges { target: root; rotation: 270 } + PropertyChanges { target: flipX; angle: 180 } + } + ] + + transitions: [ + Transition { + from: ",rot90,rot180,rot270"; to: from + enabled: enableAnimations + + PieceRotationAnimation { } + }, + Transition { + from: "flip,rot90Flip,rot180Flip,rot270Flip"; to: from + enabled: enableAnimations + + PieceRotationAnimation { } + }, + Transition { + from: ",flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: "rot90,rot90Flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: "rot180,rot180Flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: "rot270,rot270Flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: ",rot180Flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + }, + Transition { + from: "rot90,rot270Flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + }, + Transition { + from: "rot180,flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + }, + Transition { + from: "rot270,rot90Flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + } + ] + } + + states: [ + State { + name: "picked" + when: isPicked + + ParentChange { + target: root + parent: pieceManipulator + x: pieceManipulator.width / 2 + y: pieceManipulator.height / 2 + } + }, + State { + name: "played" + when: pieceModel.isPlayed + + ParentChange { + target: root + parent: board + x: board.mapFromGameX(pieceModel.gameCoord.x) + y: board.mapFromGameY(pieceModel.gameCoord.y) + } + }, + State { + name: "unplayed" + when: parentUnplayed != null + + PropertyChanges { + target: root + // Avoid fractional sizes for square piece elements + scale: Math.floor(0.25 * parentUnplayed.width) / gridWidth + } + ParentChange { + target: root + parent: parentUnplayed + x: parentUnplayed.width / 2 + y: parentUnplayed.height / 2 + } + } + ] + transitions: + Transition { + from: "unplayed,picked,played"; to: from + enabled: enableAnimations + + ParentAnimation { + via: gameDisplay + NumberAnimation { + properties: "x,y,scale" + duration: 300 + easing.type: Easing.InOutQuad + } + } + } +} diff --git a/src/pentobi_qml/qml/PieceClassic.qml b/src/pentobi_qml/qml/PieceClassic.qml new file mode 100644 index 0000000..afa9041 --- /dev/null +++ b/src/pentobi_qml/qml/PieceClassic.qml @@ -0,0 +1,259 @@ +import QtQuick 2.0 + +Item +{ + id: root + + property var pieceModel + property string colorName + property bool isPicked + property Item parentUnplayed + property real gridWidth: board.gridWidth + property real gridHeight: board.gridHeight + property bool isMarked + property string imageName: theme.getImage("square-" + colorName) + property real pieceAngle: { + var flX = Math.abs(flipX.angle % 360 - 180) < 90 + var flY = Math.abs(flipY.angle % 360 - 180) < 90 + var angle = rotation + if (flX && flY) angle += 180 + else if (flX) angle += 90 + else if (flY) angle += 270 + return angle + } + property real imageOpacity0: imageOpacity(pieceAngle, 0) + property real imageOpacity90: imageOpacity(pieceAngle, 90) + property real imageOpacity180: imageOpacity(pieceAngle, 180) + property real imageOpacity270: imageOpacity(pieceAngle, 270) + + z: 1 // Must be above board and piece manipulator during transition + transform: [ + Rotation { + id: flipX + + axis { x: 1; y: 0; z: 0 } + origin { x: width / 2; y: height / 2 } + }, + Rotation { + id: flipY + + axis { x: 0; y: 1; z: 0 } + origin { x: width / 2; y: height / 2 } + } + ] + + function imageOpacity(pieceAngle, imgAngle) { + var angle = (((pieceAngle - imgAngle) % 360) + 360) % 360 // JS modulo bug + return (angle >= 90 && angle <= 270 ? 0 : Math.cos(angle * Math.PI / 180)) + } + + Repeater { + model: pieceModel.elements + + Square { + width: gridWidth + height: gridHeight + x: (modelData.x - pieceModel.center.x) * gridWidth + y: (modelData.y - pieceModel.center.y) * gridHeight + } + } + Rectangle { + opacity: isMarked ? 0.5 : 0 + color: colorName == "blue" || colorName == "red" ? + "white" : "#333333" + width: 0.3 * gridHeight + height: width + radius: width / 2 + x: (pieceModel.labelPos.x - pieceModel.center.x + 0.5) + * gridWidth - width / 2 + y: (pieceModel.labelPos.y - pieceModel.center.y + 0.5) + * gridHeight - height / 2 + Behavior on opacity { NumberAnimation { duration: 80 } } + } + StateGroup { + state: pieceModel.state + + states: [ + State { + name: "rot90" + PropertyChanges { target: root; rotation: 90 } + }, + State { + name: "rot180" + PropertyChanges { target: root; rotation: 180 } + }, + State { + name: "rot270" + PropertyChanges { target: root; rotation: 270 } + }, + State { + name: "flip" + PropertyChanges { target: flipX; angle: 180 } + }, + State { + name: "rot90Flip" + PropertyChanges { target: root; rotation: 90 } + PropertyChanges { target: flipX; angle: 180 } + }, + State { + name: "rot180Flip" + PropertyChanges { target: root; rotation: 180 } + PropertyChanges { target: flipX; angle: 180 } + }, + State { + name: "rot270Flip" + PropertyChanges { target: root; rotation: 270 } + PropertyChanges { target: flipX; angle: 180 } + } + ] + + // Unique states are defined by rotating and flipping around the x axis + // but for some transitions, the shortest visual animation is flipping + // around the y axis. + transitions: [ + Transition { + from: ",rot90,rot180,rot270"; to: from + enabled: enableAnimations + + PieceRotationAnimation { } + }, + Transition { + from: "flip,rot90Flip,rot180Flip,rot270Flip"; to: from + enabled: enableAnimations + + PieceRotationAnimation { } + }, + Transition { + from: ",flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: "rot90,rot90Flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: "rot180,rot180Flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: "rot270,rot270Flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: ",rot180Flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + }, + Transition { + from: "rot90,rot270Flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + }, + Transition { + from: "rot180,flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + }, + Transition { + from: "rot270,rot90Flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + } + ] + } + + states: [ + State { + name: "picked" + when: isPicked + + ParentChange { + target: root + parent: pieceManipulator + x: pieceManipulator.width / 2 + y: pieceManipulator.height / 2 + } + }, + State { + name: "played" + when: pieceModel.isPlayed + + ParentChange { + target: root + parent: board + x: board.mapFromGameX(pieceModel.gameCoord.x) + y: board.mapFromGameY(pieceModel.gameCoord.y) + } + }, + State { + name: "unplayed" + when: parentUnplayed != null + + PropertyChanges { + target: root + // Avoid fractional sizes for square piece elements + scale : Math.floor(0.2 * parentUnplayed.width) / gridWidth + } + ParentChange { + target: root + parent: parentUnplayed + x: parentUnplayed.width / 2 + y: parentUnplayed.height / 2 + } + } + ] + transitions: + Transition { + from: "unplayed,picked,played"; to: from + enabled: enableAnimations + + ParentAnimation { + via: gameDisplay + NumberAnimation { + properties: "x,y,scale" + duration: 300 + easing.type: Easing.InOutQuad + } + } + } +} diff --git a/src/pentobi_qml/qml/PieceFlipAnimation.qml b/src/pentobi_qml/qml/PieceFlipAnimation.qml new file mode 100644 index 0000000..9c43deb --- /dev/null +++ b/src/pentobi_qml/qml/PieceFlipAnimation.qml @@ -0,0 +1,7 @@ +import QtQuick 2.0 + +RotationAnimation { + duration: 300 + direction: RotationAnimation.Shortest + property: "angle" +} diff --git a/src/pentobi_qml/qml/PieceList.qml b/src/pentobi_qml/qml/PieceList.qml new file mode 100644 index 0000000..4caa196 --- /dev/null +++ b/src/pentobi_qml/qml/PieceList.qml @@ -0,0 +1,26 @@ +import QtQuick 2.0 + +Grid { + id: root + + property var pieces + + signal piecePicked(var piece) + + opacity: theme.pieceListOpacity + + Repeater { + model: pieces + + MouseArea { + id: mouseArea + + property var piece: modelData + + width: root.width / columns; height: width + visible: ! piece.pieceModel.isPlayed + onClicked: piecePicked(piece) + Component.onCompleted: piece.parentUnplayed = mouseArea + } + } +} diff --git a/src/pentobi_qml/qml/PieceManipulator.qml b/src/pentobi_qml/qml/PieceManipulator.qml new file mode 100644 index 0000000..9a06b5b --- /dev/null +++ b/src/pentobi_qml/qml/PieceManipulator.qml @@ -0,0 +1,72 @@ +import QtQuick 2.0 + +Item { + id: root + + property var pieceModel + // True if piece manipulator is at a board location that is a legal move + property bool legal + + signal piecePlayed + + Image { + anchors.fill: root + source: theme.getImage("piece-manipulator") + sourceSize { width: width; height: height } + opacity: ! legal ? 0.4 : 0 + Behavior on opacity { NumberAnimation { duration: 100 } } + } + Image { + anchors.fill: root + source: theme.getImage("piece-manipulator-legal") + sourceSize { width: width; height: height } + opacity: legal ? 0.4 : 0 + Behavior on opacity { NumberAnimation { duration: 100 } } + } + MouseArea { + id: dragArea + + anchors.fill: root + drag { + target: root + filterChildren: true + minimumX: -width / 2; maximumX: root.parent.width - width / 2 + minimumY: -height / 2; maximumY: root.parent.height - height / 2 + } + + MouseArea { + anchors.centerIn: dragArea + width: 0.5 * root.width; height: width + onClicked: piecePlayed() + } + MouseArea { + anchors { + top: dragArea.top + horizontalCenter: dragArea.horizontalCenter + } + width: 0.2 * root.width; height: width + onClicked: pieceModel.rotateRight() + } + MouseArea { + anchors { + right: dragArea.right + verticalCenter: dragArea.verticalCenter + } + width: 0.2 * root.width; height: width + onClicked: pieceModel.flipAcrossX() + } + MouseArea { + anchors { + bottom: dragArea.bottom + horizontalCenter: dragArea.horizontalCenter + } + width: 0.2 * root.width; height: width + onClicked: pieceModel.flipAcrossY() + } + MouseArea { + anchors { left: dragArea.left; verticalCenter: dragArea.verticalCenter } + width: 0.2 * root.width; height: width + onClicked: pieceModel.rotateLeft() + } + } +} diff --git a/src/pentobi_qml/qml/PieceNexos.qml b/src/pentobi_qml/qml/PieceNexos.qml new file mode 100644 index 0000000..b5b7d0a --- /dev/null +++ b/src/pentobi_qml/qml/PieceNexos.qml @@ -0,0 +1,310 @@ +import QtQuick 2.3 + +// Piece for Nexos. See PieceClassic for comments. +Item +{ + id: root + + property var pieceModel + property string colorName + property bool isPicked + property Item parentUnplayed + property real gridWidth: board.gridWidth + property real gridHeight: board.gridHeight + property bool isMarked + property string imageName: theme.getImage("linesegment-" + colorName) + property real pieceAngle: { + var flX = Math.abs(flipX.angle % 360 - 180) < 90 + var flY = Math.abs(flipY.angle % 360 - 180) < 90 + var angle = rotation + if (flX && flY) angle += 180 + else if (flX) angle += 90 + else if (flY) angle += 270 + return angle + } + property real imageOpacity0: imageOpacity(pieceAngle, 0) + property real imageOpacity90: imageOpacity(pieceAngle, 90) + property real imageOpacity180: imageOpacity(pieceAngle, 180) + property real imageOpacity270: imageOpacity(pieceAngle, 270) + + z: 1 + transform: [ + Rotation { + id: flipX + + axis { x: 1; y: 0; z: 0 } + origin { x: width / 2; y: height / 2 } + }, + Rotation { + id: flipY + + axis { x: 0; y: 1; z: 0 } + origin { x: width / 2; y: height / 2 } + } + ] + + function isHorizontal(pos) { return (pos.x % 2 != 0) } + function imageOpacity(pieceAngle, imgAngle) { + var angle = (((pieceAngle - imgAngle) % 360) + 360) % 360 + return (angle >= 90 && angle <= 270 ? 0 : Math.cos(angle * Math.PI / 180)) + } + + Repeater { + model: pieceModel.elements + + LineSegment { + isHorizontal: root.isHorizontal(modelData) + width: 1.5 * gridWidth + height: 0.5 * gridHeight + x: (modelData.x - pieceModel.center.x - 0.25) * gridWidth + y: (modelData.y - pieceModel.center.y + 0.25) * gridHeight + } + } + Repeater { + model: pieceModel.junctions + + Image { + source: { + switch (pieceModel.junctionType[index]) { + case 0: + return theme.getImage("junction-all-" + colorName) + case 1: + case 2: + case 3: + case 4: + return theme.getImage("junction-t-" + colorName) + case 5: + case 6: + return theme.getImage("junction-straight-" + colorName) + case 7: + case 8: + case 9: + case 10: + return theme.getImage("junction-rect-" + colorName) + } + } + rotation: { + switch (pieceModel.junctionType[index]) { + case 0: + case 3: + case 5: + case 10: + return 0 + case 1: + case 9: + return 270 + case 2: + case 6: + case 8: + return 90 + case 4: + case 7: + return 180 + } + } + width: 0.5 * gridWidth + height: 0.5 * gridHeight + x: (modelData.x - pieceModel.center.x + 0.25) * gridWidth + y: (modelData.y - pieceModel.center.y + 0.25) * gridHeight + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + } + } + Rectangle { + opacity: isMarked ? 0.5 : 0 + color: colorName == "blue" || colorName == "red" ? + "white" : "#333333" + width: 0.3 * gridHeight + height: width + radius: width / 2 + x: (pieceModel.labelPos.x - pieceModel.center.x + 0.5) + * gridWidth - width / 2 + y: (pieceModel.labelPos.y - pieceModel.center.y + 0.5) + * gridHeight - height / 2 + Behavior on opacity { NumberAnimation { duration: 80 } } + } + StateGroup { + state: pieceModel.state + + states: [ + State { + name: "rot90" + PropertyChanges { target: root; rotation: 90 } + }, + State { + name: "rot180" + PropertyChanges { target: root; rotation: 180 } + }, + State { + name: "rot270" + PropertyChanges { target: root; rotation: 270 } + }, + State { + name: "flip" + PropertyChanges { target: flipX; angle: 180 } + }, + State { + name: "rot90Flip" + PropertyChanges { target: root; rotation: 90 } + PropertyChanges { target: flipX; angle: 180 } + }, + State { + name: "rot180Flip" + PropertyChanges { target: root; rotation: 180 } + PropertyChanges { target: flipX; angle: 180 } + }, + State { + name: "rot270Flip" + PropertyChanges { target: root; rotation: 270 } + PropertyChanges { target: flipX; angle: 180 } + } + ] + + transitions: [ + Transition { + from: ",rot90,rot180,rot270"; to: from + enabled: enableAnimations + + PieceRotationAnimation { } + }, + Transition { + from: "flip,rot90Flip,rot180Flip,rot270Flip"; to: from + enabled: enableAnimations + + PieceRotationAnimation { } + }, + Transition { + from: ",flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: "rot90,rot90Flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: "rot180,rot180Flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: "rot270,rot270Flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: ",rot180Flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + }, + Transition { + from: "rot90,rot270Flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + }, + Transition { + from: "rot180,flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + }, + Transition { + from: "rot270,rot90Flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + } + ] + } + + states: [ + State { + name: "picked" + when: isPicked + + ParentChange { + target: root + parent: pieceManipulator + x: pieceManipulator.width / 2 + y: pieceManipulator.height / 2 + } + }, + State { + name: "played" + when: pieceModel.isPlayed + + ParentChange { + target: root + parent: board + x: board.mapFromGameX(pieceModel.gameCoord.x) + y: board.mapFromGameY(pieceModel.gameCoord.y) + } + }, + State { + name: "unplayed" + when: parentUnplayed != null + + PropertyChanges { + target: root + // Avoid fractional sizes for square piece elements + scale: Math.floor(0.12 * parentUnplayed.width) / gridWidth + } + ParentChange { + target: root + parent: parentUnplayed + x: parentUnplayed.width / 2 + y: parentUnplayed.height / 2 + } + } + ] + transitions: + Transition { + from: "unplayed,picked,played"; to: from + enabled: enableAnimations + + ParentAnimation { + via: gameDisplay + NumberAnimation { + properties: "x,y,scale" + duration: 300 + easing.type: Easing.InOutQuad + } + } + } +} diff --git a/src/pentobi_qml/qml/PieceRotationAnimation.qml b/src/pentobi_qml/qml/PieceRotationAnimation.qml new file mode 100644 index 0000000..0f2c268 --- /dev/null +++ b/src/pentobi_qml/qml/PieceRotationAnimation.qml @@ -0,0 +1,7 @@ +import QtQuick 2.0 + +RotationAnimation { + duration: 300 + direction: RotationAnimation.Shortest + property: "rotation" +} diff --git a/src/pentobi_qml/qml/PieceSelector.qml b/src/pentobi_qml/qml/PieceSelector.qml new file mode 100644 index 0000000..30c9f66 --- /dev/null +++ b/src/pentobi_qml/qml/PieceSelector.qml @@ -0,0 +1,195 @@ +import QtQuick 2.0 + +Flickable { + id: root + + property string gameVariant + property int toPlay + property var pieces0 + property var pieces1 + property var pieces2 + property var pieces3 + property int nuColors + property int columns + property int rows + property bool transitionsEnabled + + signal piecePicked(var piece) + + contentHeight: pieceList0.height + pieceList1.height + + pieceList2.height + pieceList3.height + flickableDirection: Flickable.VerticalFlick + clip: true + + PieceList { + id: pieceList0 + + width: root.width + columns: root.columns + pieces: pieces0 + onPiecePicked: root.piecePicked(piece) + } + PieceList { + id: pieceList1 + + width: root.width + columns: root.columns + pieces: pieces1 + onPiecePicked: root.piecePicked(piece) + } + PieceList { + id: pieceList2 + + width: root.width + columns: root.columns + pieces: pieces2 + onPiecePicked: root.piecePicked(piece) + } + PieceList { + id: pieceList3 + + width: root.width + columns: root.columns + pieces: pieces3 + onPiecePicked: root.piecePicked(piece) + } + + states: [ + State { + name: "toPlay0" + when: toPlay === 0 + + PropertyChanges { + target: pieceList0 + y: 0 + } + PropertyChanges { + target: pieceList1 + y: gameVariant != "classic_2" && gameVariant != "trigon_2" + && gameVariant != "nexos_2" ? + pieceList0.height : + pieceList0.height + pieceList2.height + } + PropertyChanges { + target: pieceList2 + y: gameVariant != "classic_2" && gameVariant != "trigon_2" + && gameVariant != "nexos_2" ? + pieceList0.height + pieceList1.height : + pieceList0.height + } + PropertyChanges { + target: pieceList3 + y: pieceList0.height + pieceList1.height + pieceList2.height + } + }, + State { + name: "toPlay1" + when: toPlay === 1 + + PropertyChanges { + target: pieceList1 + y: 0 + } + PropertyChanges { + target: pieceList2 + y: gameVariant != "classic_2" && gameVariant != "trigon_2" + && gameVariant != "nexos_2" ? + pieceList1.height : + pieceList1.height + pieceList3.height + } + PropertyChanges { + target: pieceList3 + y: gameVariant != "classic_2" && gameVariant != "trigon_2" + && gameVariant != "nexos_2" ? + pieceList1.height + pieceList2.height : + pieceList1.height + } + PropertyChanges { + target: pieceList0 + y: pieceList1.height + pieceList2.height + pieceList3.height + } + }, + State { + name: "toPlay2" + when: toPlay === 2 + + PropertyChanges { + target: pieceList2 + y: 0 + } + PropertyChanges { + target: pieceList3 + y: gameVariant != "classic_2" && gameVariant != "trigon_2" + && gameVariant != "nexos_2" ? + pieceList2.height : + pieceList2.height + pieceList0.height + } + PropertyChanges { + target: pieceList0 + y: gameVariant != "classic_2" && gameVariant != "trigon_2" + && gameVariant != "nexos_2" ? + pieceList2.height + pieceList3.height : + pieceList2.height + } + PropertyChanges { + target: pieceList1 + y: pieceList2.height + pieceList3.height + pieceList0.height + } + }, + State { + name: "toPlay3" + when: toPlay === 3 + + PropertyChanges { + target: pieceList3 + y: 0 + } + PropertyChanges { + target: pieceList0 + y: gameVariant != "classic_2" && gameVariant != "trigon_2" + && gameVariant != "nexos_2" ? + pieceList3.height : + pieceList3.height + pieceList1.height + } + PropertyChanges { + target: pieceList1 + y: gameVariant != "classic_2" && gameVariant != "trigon_2" + && gameVariant != "nexos_2" ? + pieceList3.height + pieceList0.height : + pieceList3.height + } + PropertyChanges { + target: pieceList2 + y: pieceList3.height + pieceList0.height + pieceList1.height + } + } + ] + transitions: + Transition { + enabled: transitionsEnabled + + SequentialAnimation { + PropertyAction { + target: pieceList0; property: "y"; value: pieceList0.y } + PropertyAction { + target: pieceList1; property: "y"; value: pieceList1.y } + PropertyAction { + target: pieceList2; property: "y"; value: pieceList2.y } + PropertyAction { + target: pieceList3; property: "y"; value: pieceList3.y } + // Delay showing new color because of piece placement animation + PauseAnimation { duration: 200 } + NumberAnimation { + target: root; property: "opacity"; to: 0; duration: 100 + } + PropertyAction { target: pieceList0; property: "y" } + PropertyAction { target: pieceList1; property: "y" } + PropertyAction { target: pieceList2; property: "y" } + PropertyAction { target: pieceList3; property: "y" } + PropertyAction { target: root; property: "contentY"; value: 0 } + NumberAnimation { + target: root; property: "opacity"; to: 1; duration: 100 + } + } + } +} diff --git a/src/pentobi_qml/qml/PieceTrigon.qml b/src/pentobi_qml/qml/PieceTrigon.qml new file mode 100644 index 0000000..91436ea --- /dev/null +++ b/src/pentobi_qml/qml/PieceTrigon.qml @@ -0,0 +1,320 @@ +import QtQuick 2.0 + +// See PieceClassic.qml for comments +Item +{ + id: root + + property var pieceModel + property string colorName + property bool isPicked + property Item parentUnplayed + property real gridWidth: board.gridWidth + property real gridHeight: board.gridHeight + property bool isMarked + property string imageName: theme.getImage("triangle-" + colorName) + property string imageNameDownward: + theme.getImage("triangle-down-" + colorName) + property real pieceAngle: { + var flX = Math.abs(flipX.angle % 360 - 180) < 90 + var flY = Math.abs(flipY.angle % 360 - 180) < 90 + var angle = rotation + if (flX && flY) angle += 180 + else if (flX) angle += 120 + else if (flY) angle += 300 + return angle + } + property real imageOpacity0: imageOpacity(pieceAngle, 0) + property real imageOpacity60: imageOpacity(pieceAngle, 60) + property real imageOpacity120: imageOpacity(pieceAngle, 120) + property real imageOpacity180: imageOpacity(pieceAngle, 180) + property real imageOpacity240: imageOpacity(pieceAngle, 240) + property real imageOpacity300: imageOpacity(pieceAngle, 300) + + z: 1 + transform: [ + Rotation { + id: flipX + + axis { x: 1; y: 0; z: 0 } + origin { x: width / 2; y: height / 2 } + }, + Rotation { + id: flipY + + axis { x: 0; y: 1; z: 0 } + origin { x: width / 2; y: height / 2 } + } + ] + + function _isDownward(pos) { return (pos.x % 2 == 0) != (pos.y % 2 == 0) } + function imageOpacity(pieceAngle, imgAngle) { + var angle = (((pieceAngle - imgAngle) % 360) + 360) % 360 + return (angle >= 60 && angle <= 300 ? 0 : 2 * Math.cos(angle * Math.PI / 180) - 1) + } + + Repeater { + model: pieceModel.elements + + Triangle { + isDownward: _isDownward(modelData) + width: 2 * gridWidth + height: gridHeight + x: (modelData.x - pieceModel.center.x - 0.5) * gridWidth + y: (modelData.y - pieceModel.center.y) * gridHeight + } + } + Rectangle { + opacity: isMarked ? 0.5 : 0 + color: colorName == "blue" || colorName == "red" ? + "white" : "#333333" + width: 0.3 * gridHeight + height: width + radius: width / 2 + x: (pieceModel.labelPos.x - pieceModel.center.x + 0.5) + * gridWidth - width / 2 + y: (pieceModel.labelPos.y - pieceModel.center.y + + (_isDownward(pieceModel.labelPos) ? 1 : 2) / 3) + * gridHeight - height / 2 + Behavior on opacity { NumberAnimation { duration: 80 } } + } + StateGroup { + state: pieceModel.state + + states: [ + State { + name: "rot60" + PropertyChanges { target: root; rotation: 60 } + }, + State { + name: "rot120" + PropertyChanges { target: root; rotation: 120 } + }, + State { + name: "rot180" + PropertyChanges { target: root; rotation: 180 } + }, + State { + name: "rot240" + PropertyChanges { target: root; rotation: 240 } + }, + State { + name: "rot300" + PropertyChanges { target: root; rotation: 300 } + }, + State { + name: "flip" + PropertyChanges { target: flipX; angle: 180 } + }, + State { + name: "rot60Flip" + PropertyChanges { target: root; rotation: 60 } + PropertyChanges { target: flipX; angle: 180 } + }, + State { + name: "rot120Flip" + PropertyChanges { target: root; rotation: 120 } + PropertyChanges { target: flipX; angle: 180 } + }, + State { + name: "rot180Flip" + PropertyChanges { target: root; rotation: 180 } + PropertyChanges { target: flipX; angle: 180 } + }, + State { + name: "rot240Flip" + PropertyChanges { target: root; rotation: 240 } + PropertyChanges { target: flipX; angle: 180 } + }, + State { + name: "rot300Flip" + PropertyChanges { target: root; rotation: 300 } + PropertyChanges { target: flipX; angle: 180 } + } + ] + + transitions: [ + Transition { + from: ",rot60,rot120,rot180,rot240,rot300"; to: from + enabled: enableAnimations + + PieceRotationAnimation { } + }, + Transition { + from: "flip,rot60Flip,rot120Flip,rot180Flip,rot240Flip,rot300Flip"; to: from + enabled: enableAnimations + + PieceRotationAnimation { } + }, + Transition { + from: ",flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: "rot60,rot60Flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: "rot120,rot120Flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: "rot180,rot180Flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: "rot240,rot240Flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: "rot300,rot300Flip"; to: from + enabled: enableAnimations + + PieceFlipAnimation { target: flipX } + }, + Transition { + from: ",rot180Flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + }, + Transition { + from: "rot60,rot240Flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + }, + Transition { + from: "rot120,rot300Flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + }, + Transition { + from: "rot180,flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + }, + Transition { + from: "rot240,rot60Flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + }, + Transition { + from: "rot300,rot120Flip"; to: from + enabled: enableAnimations + + SequentialAnimation { + PropertyAction { property: "rotation"; value: rotation } + PropertyAction { + target: flipX; property: "angle"; value: flipX.angle + } + PieceFlipAnimation { target: flipY; to: 180 } + PropertyAction { target: flipY; property: "angle"; value: 0 } + } + } + ] + } + + states: [ + State { + name: "picked" + when: isPicked + + ParentChange { + target: root + parent: pieceManipulator + x: pieceManipulator.width / 2 + y: pieceManipulator.height / 2 + } + }, + State { + name: "played" + when: pieceModel.isPlayed + + ParentChange { + target: root + parent: board + x: board.mapFromGameX(pieceModel.gameCoord.x) + y: board.mapFromGameY(pieceModel.gameCoord.y) + } + }, + State { + name: "unplayed" + when: parentUnplayed != null + + PropertyChanges { + target: root + scale: 0.13 * parentUnplayed.width / gridWidth + } + ParentChange { + target: root + parent: parentUnplayed + x: parentUnplayed.width / 2 + y: parentUnplayed.height / 2 + } + } + ] + + transitions: + Transition { + from: "unplayed,picked,played"; to: from + enabled: enableAnimations + + ParentAnimation { + via: gameDisplay + NumberAnimation { + properties: "x,y,scale" + duration: 300 + easing.type: Easing.InOutQuad + } + } + } +} diff --git a/src/pentobi_qml/qml/SaveDialog.qml b/src/pentobi_qml/qml/SaveDialog.qml new file mode 100644 index 0000000..1a59687 --- /dev/null +++ b/src/pentobi_qml/qml/SaveDialog.qml @@ -0,0 +1,16 @@ +import QtQuick 2.0 +import QtQuick.Dialogs 1.2 +import "Main.js" as Logic + +FileDialog { + title: qsTr("Save") + selectExisting: false + folder: root.folder == "" ? shortcuts.desktop : root.folder + nameFilters: [ qsTr("Blokus games (*.blksgf)"), qsTr("All files (*)") ] + onAccepted: { + Logic.saveFileUrl(fileUrl) + root.folder = folder + gameDisplay.forceActiveFocus() // QTBUG-48456 + } + onRejected: gameDisplay.forceActiveFocus() // QTBUG-48456 +} diff --git a/src/pentobi_qml/qml/ScoreDisplay.qml b/src/pentobi_qml/qml/ScoreDisplay.qml new file mode 100644 index 0000000..7e842a1 --- /dev/null +++ b/src/pentobi_qml/qml/ScoreDisplay.qml @@ -0,0 +1,110 @@ +import QtQuick 2.0 + +Row { + id: root + + property real pointSize + property int toPlay + property int altPlayer + property string gameVariant + property real points0 + property real points1 + property real points2 + property real points3 + property real bonus0 + property real bonus1 + property real bonus2 + property real bonus3 + property bool hasMoves0 + property bool hasMoves1 + property bool hasMoves2 + property bool hasMoves3 + + ScoreElement2 { + visible: gameVariant == "classic_2" || gameVariant == "trigon_2" + || gameVariant == "nexos_2" + value: points0 + points2 + isFinal: ! hasMoves0 && ! hasMoves2 + pointSize: root.pointSize + height: root.height + width: 5.9 * pointSize + color1: theme.colorBlue + color2: theme.colorRed + } + ScoreElement2 { + visible: gameVariant == "classic_2" || gameVariant == "trigon_2" + || gameVariant == "nexos_2" + value: points1 + points3 + isFinal: ! hasMoves1 && ! hasMoves3 + pointSize: root.pointSize + height: root.height + width: 5.9 * pointSize + color1: theme.colorYellow + color2: theme.colorGreen + } + ScoreElement { + value: points0 + bonus: bonus0 + isFinal: ! hasMoves0 + isToPlay: toPlay == 0 + pointSize: root.pointSize + height: root.height + width: 5 * pointSize + color: theme.colorBlue + } + ScoreElement { + value: points1 + bonus: bonus1 + isFinal: ! hasMoves1 + isToPlay: toPlay == 1 + pointSize: root.pointSize + height: root.height + width: 5 * pointSize + color: gameModel.gameVariant == "duo" + || gameModel.gameVariant == "junior" + || gameModel.gameVariant == "callisto_2" ? + theme.colorGreen : theme.colorYellow + } + ScoreElement { + visible: gameVariant != "duo" && gameVariant != "junior" + && gameVariant != "callisto_2" + value: points2 + bonus: bonus2 + isFinal: ! hasMoves2 + isToPlay: toPlay == 2 + pointSize: root.pointSize + height: root.height + width: 5 * pointSize + color: theme.colorRed + } + ScoreElement { + visible: gameVariant != "duo" && gameVariant != "junior" + && gameVariant != "callisto_2" && gameVariant != "trigon_3" + && gameVariant != "classic_3" && gameVariant != "callisto_3" + value: points3 + bonus: bonus3 + isFinal: ! hasMoves3 + isToPlay: toPlay == 3 + pointSize: root.pointSize + height: root.height + width: 5 * pointSize + color: theme.colorGreen + } + ScoreElement2 { + visible: gameVariant == "classic_3" + value: points3 + isAltColor: true + isToPlay: toPlay == 3 + isFinal: ! hasMoves3 + pointSize: root.pointSize + height: root.height + width: 5.9 * pointSize + color1: theme.colorGreen + color2: + switch (altPlayer) { + case 0: return theme.colorBlue + case 1: return theme.colorYellow + case 2: return theme.colorRed + } + } +} diff --git a/src/pentobi_qml/qml/ScoreElement.qml b/src/pentobi_qml/qml/ScoreElement.qml new file mode 100644 index 0000000..1211cf4 --- /dev/null +++ b/src/pentobi_qml/qml/ScoreElement.qml @@ -0,0 +1,40 @@ +import QtQuick 2.0 + +Item { + id: root + + property alias color: point.color + property bool isFinal + property bool isToPlay + property real value + property real bonus + property real pointSize + + Rectangle { + id: point + + width: (isToPlay ? 1.3 : 1) * pointSize + border { + color: Qt.lighter(color, theme.toPlayColorLighter) + width: isToPlay ? Math.max(0.15 * pointSize, 1) : 0 + } + height: width + radius: width / 2 + anchors.verticalCenter: root.verticalCenter + } + Text { + id: scoreText + + text: ! isFinal ? + value : (bonus > 0 ? "*" : "") + "" + value + "" + color: theme.fontColorScore + anchors { + left: point.right + leftMargin: (isToPlay ? 0.2 : 0.4) * point.width + verticalCenter: root.verticalCenter + } + verticalAlignment: Text.AlignVCenter + renderType: Text.NativeRendering + font.pixelSize: 1.4 * pointSize + } +} diff --git a/src/pentobi_qml/qml/ScoreElement2.qml b/src/pentobi_qml/qml/ScoreElement2.qml new file mode 100644 index 0000000..436c15a --- /dev/null +++ b/src/pentobi_qml/qml/ScoreElement2.qml @@ -0,0 +1,58 @@ +import QtQuick 2.0 + +Item { + id: root + + property color color1 + property color color2 + property bool isFinal + property bool isToPlay + property bool isAltColor + property real value + property real pointSize + + Rectangle { + id: point1 + + color: color1 + opacity: isAltColor && isFinal ? 0 : 1 + width: (isToPlay ? 1.3 : 1) * pointSize + border { + color: Qt.lighter(color1, theme.toPlayColorLighter) + width: isToPlay ? Math.max(0.15 * pointSize, 1) : 0 + } + height: width + radius: width / 2 + anchors.verticalCenter: root.verticalCenter + } + Rectangle { + id: point2 + + color: isAltColor && isFinal ? color1 : color2 + width: pointSize + height: width + radius: width / 2 + anchors { + left: point1.right + verticalCenter: root.verticalCenter + } + } + Text { + text: { + if (isAltColor) + return isFinal ? "(" + value + ")" : "(" + value + ")" + else + return isFinal ? "" + value + "" : value + } + color: theme.fontColorScore + width: root.width - point1.width - point2.width - anchors.leftMargin + anchors { + left: point2.right + leftMargin: (isToPlay ? 0.2 : 0.4) * point1.width + verticalCenter: root.verticalCenter + } + verticalAlignment: Text.AlignVCenter + renderType: Text.NativeRendering + font.pixelSize: 1.4 * pointSize + } +} diff --git a/src/pentobi_qml/qml/Square.qml b/src/pentobi_qml/qml/Square.qml new file mode 100644 index 0000000..a2901ec --- /dev/null +++ b/src/pentobi_qml/qml/Square.qml @@ -0,0 +1,100 @@ +import QtQuick 2.3 + +// Piece element (square) with pseudo-3D effect. +// Simulates lighting by using different images for the lighting at different +// rotations and interpolating between them with an opacity animation. +Item { + id: root + + Loader { + function loadImage() { + if (opacity > 0 && status === Loader.Null) + sourceComponent = component0 + } + + anchors.fill: root + opacity: imageOpacity0 + onOpacityChanged: loadImage() + Component.onCompleted: loadImage() + + Component { + id: component0 + + Image { + source: imageName + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + } + } + } + Loader { + function loadImage() { + if (opacity > 0 && status === Loader.Null) + sourceComponent = component90 + } + + anchors.fill: root + opacity: imageOpacity90 + onOpacityChanged: loadImage() + Component.onCompleted: loadImage() + + Component { + id: component90 + + Image { + source: imageName + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + rotation: -90 + } + } + } + Loader { + function loadImage() { + if (opacity > 0 && status === Loader.Null) + sourceComponent = component180 + } + + anchors.fill: root + opacity: imageOpacity180 + onOpacityChanged: loadImage() + Component.onCompleted: loadImage() + + Component { + id: component180 + + Image { + source: imageName + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + rotation: -180 + } + } + } + Loader { + function loadImage() { + if (opacity > 0 && status === Loader.Null) + sourceComponent = component270 + } + + anchors.fill: root + opacity: imageOpacity270 + onOpacityChanged: loadImage() + Component.onCompleted: loadImage() + + Component { + id: component270 + + Image { + source: imageName + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + rotation: -270 + } + } + } +} diff --git a/src/pentobi_qml/qml/ToolBar.qml b/src/pentobi_qml/qml/ToolBar.qml new file mode 100644 index 0000000..d478f6a --- /dev/null +++ b/src/pentobi_qml/qml/ToolBar.qml @@ -0,0 +1,30 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.1 +import QtQuick.Layouts 1.1 +import "Main.js" as Logic + +ToolBar { + RowLayout { + anchors.fill: parent + + ToolButton { + iconSource: "icons/pentobi-newgame.svg" + enabled: ! gameModel.isGameEmpty + onClicked: Logic.newGame() + } + ToolButton { + iconSource: "icons/pentobi-undo.svg" + enabled: gameModel.canUndo + onClicked: Logic.undo() + } + ToolButton { + iconSource: "icons/pentobi-computer-colors.svg" + onClicked: Logic.showComputerColorDialog() + } + ToolButton { + iconSource: "icons/pentobi-play.svg" + onClicked: Logic.computerPlay() + } + Item { Layout.fillWidth: true } + } +} diff --git a/src/pentobi_qml/qml/Triangle.qml b/src/pentobi_qml/qml/Triangle.qml new file mode 100644 index 0000000..79083c8 --- /dev/null +++ b/src/pentobi_qml/qml/Triangle.qml @@ -0,0 +1,176 @@ +import QtQuick 2.3 + +// See Square.qml for comments +Item { + id: root + + property bool isDownward + + Loader { + function loadImage() { + if (opacity > 0 && status === Loader.Null) + sourceComponent = component0 + } + + anchors.fill: root + opacity: imageOpacity0 + onOpacityChanged: loadImage() + Component.onCompleted: loadImage() + + Component { + id: component0 + + Image { + source: isDownward ? imageNameDownward : imageName + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + } + } + } + Loader { + function loadImage() { + if (opacity > 0 && status === Loader.Null) + sourceComponent = component60 + } + + anchors.fill: root + opacity: imageOpacity60 + onOpacityChanged: loadImage() + Component.onCompleted: loadImage() + + Component { + id: component60 + + Image { + source: isDownward ? imageName : imageNameDownward + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + transform: [ + Rotation { + angle: -60 + origin { + x: width / 2 + y: isDownward ? 2 * height / 3 : height / 3 + } + }, + Translate { y: isDownward ? -height / 3 : height / 3 } + ] + } + } + } + Loader { + function loadImage() { + if (opacity > 0 && status === Loader.Null) + sourceComponent = component120 + } + + anchors.fill: root + opacity: imageOpacity120 + onOpacityChanged: loadImage() + Component.onCompleted: loadImage() + + Component { + id: component120 + + Image { + source: isDownward ? imageNameDownward : imageName + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + transform: Rotation { + angle: -120 + origin { + x: width / 2 + y: isDownward ? height / 3 : 2 * height / 3 + } + } + } + } + } + Loader { + function loadImage() { + if (opacity > 0 && status === Loader.Null) + sourceComponent = component180 + } + + anchors.fill: root + opacity: imageOpacity180 + onOpacityChanged: loadImage() + Component.onCompleted: loadImage() + + Component { + id: component180 + + Image { + source: isDownward ? imageName : imageNameDownward + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + rotation: -180 + } + } + } + Loader { + function loadImage() { + if (opacity > 0 && status === Loader.Null) + sourceComponent = component240 + } + + anchors.fill: root + opacity: imageOpacity240 + onOpacityChanged: loadImage() + Component.onCompleted: loadImage() + + Component { + id: component240 + + Image { + source: isDownward ? imageNameDownward : imageName + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + transform: Rotation { + angle: -240 + origin { + x: width / 2 + y: isDownward ? height / 3 : 2 * height / 3 + } + } + } + } + } + Loader { + function loadImage() { + if (opacity > 0 && status === Loader.Null) + sourceComponent = component300 + } + + anchors.fill: root + opacity: imageOpacity300 + onOpacityChanged: loadImage() + Component.onCompleted: loadImage() + + Component { + id: component300 + + Image { + source: isDownward ? imageName : imageNameDownward + sourceSize: imageSourceSize + mipmap: true + antialiasing: true + transform: [ + Rotation { + angle: -300 + origin { + x: width / 2 + y: isDownward ? 2 * height / 3 : height / 3 + } + }, + Translate { y: isDownward ? -height / 3 : height / 3 } + ] + } + } + } +} diff --git a/src/pentobi_qml/qml/i18n/qml_de.ts b/src/pentobi_qml/qml/i18n/qml_de.ts new file mode 100644 index 0000000..9e7af56 --- /dev/null +++ b/src/pentobi_qml/qml/i18n/qml_de.ts @@ -0,0 +1,480 @@ + + + + + ComputerColorDialog + + Computer Colors + Computer-Farben + + + Computer plays: + Computer spielt: + + + Blue/Red + Blau/Rot + + + Blue + Blau + + + Yellow/Green + Gelb/Grün + + + Green + Grün + + + Yellow + Gelb + + + Red + Rot + + + + GameModel + + (Setup) + (Stellung) + + + (No moves) + (Keine Züge) + + + Move %1 + Zug %1 + + + Blue wins with 1 point. + Blau gewinnt mit 1 Punkt. + + + Blue wins with %1 points. + Blau gewinnt mit %1 Punkten. + + + Green wins with 1 point. + Grün gewinnt mit 1 Punkt. + + + Green wins with %1 points. + Grün gewinnt mit %1 Punkten. + + + Green wins (tie resolved). + Grün gewinnt (Unentschieden aufgelöst). + + + Game ends in a tie. + Spiel endet unentschieden. + + + Blue/Red wins with 1 point. + Blau/Rot gewinnt mit 1 Punkt. + + + Blue/Red wins with %1 points. + Blau/Rot gewinnt mit %1 Punkten. + + + Yellow/Green wins with 1 point. + Gelb/Grün gewinnt mit 1 Punkt. + + + Yellow/Green wins with %1 points. + Gelb/Grün gewinnt mit %1 Punkten. + + + Yellow/Green wins (tie resolved). + Gelb/Grün gewinnt (Unentschieden aufgelöst). + + + Blue wins. + Blau gewinnt. + + + Yellow wins. + Gelb gewinnt. + + + Red wins. + Rot gewinnt. + + + Red wins (tie resolved). + Rot gewinnt (Unentschieden aufgelöst). + + + Yellow wins (tie resolved). + Gelb gewinnt (Unentschieden aufgelöst). + + + Game ends in a tie between Blue and Yellow. + Spiel endet in einem Unentschieden zwischen Blau und Gelb. + + + Game ends in a tie between Blue and Red. + Spiel endet in einem Unentschieden zwischen Blau und Rot. + + + Game ends in a tie between Yellow and Red. + Spiel endet in einem Unentschieden zwischen Gelb und Rot. + + + Game ends in a tie between all players. + Spiel endet in einem Unentschieden zwischen allen Spielern. + + + Green wins. + Grün gewinnt. + + + Game ends in a tie between Blue, Yellow and Red. + Spiel endet in einem Unentschieden zwischen Blau, Gelb und Rot. + + + Game ends in a tie between Blue, Yellow and Green. + Spiel endet in einem Unentschieden zwischen Blau, Gelb und Grün. + + + Game ends in a tie between Blue, Red and Green. + Spiel endet in einem Unentschieden zwischen Blau, Rot und Grün. + + + Game ends in a tie between Yellow, Red and Green. + Spiel endet in einem Unentschieden zwischen Gelb, Rot und Grün. + + + + Main + + Pentobi + Pentobi + + + New game? + Neues Spiel? + + + Open failed. + Öffnen fehlgeschlagen. + + + Save failed. + Speichern fehlgeschlagen. + + + Truncate this subtree? + Diesen Teilbaum abschneiden? + + + Truncate children? + Kindknoten abschneiden? + + + Delete all variations? + Alle Varianten löschen? + + + Version %1 + Version %1 + + + Computer opponent for the board game Blokus. + Computer-Gegner für das Brettspiel Blokus. + + + &copy; 2011&ndash;%1 Markus&nbsp;Enzenberger + &copy; 2011&ndash;%1 Markus&nbsp;Enzenberger + + + + MenuComputer + + &Computer + &Computer + + + Computer &Colors + Computer-&Farben + + + &Play + &Spielen + + + &Level (Classic, 4 Players) + Spielst&ufe (Klassisch, 4 Spieler) + + + &Level (Classic, 2 Players) + Spielst&ufe (Klassisch, 2 Spieler) + + + &Level (Classic, 3 Players) + Spielst&ufe (Klassisch, 3 Spieler) + + + &Level (Duo) + Spielst&ufe (Duo) + + + &Level (Junior) + Spielst&ufe (Junior) + + + &Level (Trigon, 4 Players) + Spielst&ufe (Trigon, 4 Spieler) + + + &Level (Trigon, 2 Players) + Spielst&ufe (Trigon, 2 Spieler) + + + &Level (Trigon, 3 Players) + Spielst&ufe (Trigon, 3 Spieler) + + + &Level (Nexos, 4 Players) + Spielst&ufe (Nexos, 4 Spieler) + + + &Level (Nexos, 2 Players) + Spielst&ufe (Nexos, 2 Spieler) + + + &Level (Callisto, 4 Players) + Spielst&ufe (Callisto, 4 Spieler) + + + &Level (Callisto, 2 Players) + Spielst&ufe (Callisto, 2 Spieler) + + + &Level (Callisto, 3 Players) + Spielst&ufe (Callisto, 3 Spieler) + + + + MenuEdit + + &Edit + &Bearbeiten + + + Make &Main Variation + Zu &Hauptvariante machen + + + Move Variation &Up + Variante nach &oben schieben + + + Move Variation &Down + Variante nach &unten schieben + + + &Truncate + &Abschneiden + + + Truncate &Children + &Kindknoten abschneiden + + + &Delete All Variations + Alle &Varianten löschen + + + &Next Color + &Nächste Farbe + + + + MenuGame + + &Game + &Spiel + + + &New + &Neu + + + Game &Variant + Spiel&variante + + + Classic (&3 Players) + Klassisch (&3 Spieler) + + + Classic (&4 Players) + Klassisch (&4 Spieler) + + + &Duo + &Duo + + + &Junior + &Junior + + + &Undo Move + Zug &rückgängig + + + &Find Move + Zug &finden + + + &Open... + Öffn&en ... + + + &Save As... + &Speichern unter ... + + + &Quit + &Beenden + + + &Classic + &Klassisch + + + Classic (&2 Players) + Klassisch (&2 Spieler) + + + &Trigon + &Trigon + + + Trigon (&2 Players) + Trigon (&2 Spieler) + + + Trigon (&3 Players) + Trigon (&3 Spieler) + + + Trigon (&4 Players) + Trigon (&4 Spieler) + + + &Nexos + &Nexos + + + Nexos (&2 Players) + Nexos (&2 Spieler) + + + Nexos (&4 Players) + Nexos (&4 Spieler) + + + C&allisto + &Callisto + + + Callisto (&2 Players) + Callisto (&2 Spieler) + + + Callisto (&3 Players) + Callisto (&3 Spieler) + + + Callisto (&4 Players) + Callisto (&4 Spieler) + + + + MenuGo + + G&o + &Gehe zu + + + Back to &Main Variation + Zurück zu &Hauptvariante + + + + MenuHelp + + &Help + &Hilfe + + + &About Pentobi + Über &Pentobi + + + + MenuView + + &View + &Ansicht + + + Mark &Last Move + &Letzten Zug markieren + + + &Animate Pieces + Spielsteine &animieren + + + + OpenDialog + + Open + Öffnen + + + Blokus games (*.blksgf) + Blokus-Partien (*.blksgf) + + + All files (*) + Alle Dateien (*) + + + + SaveDialog + + Save + Speichern + + + Blokus games (*.blksgf) + Blokus-Partien (*.blksgf) + + + All files (*) + Alle Dateien (*) + + + + main + + Not enough memory. + Nicht genügend Speicher. + + + Pentobi + Pentobi + + + diff --git a/src/pentobi_qml/qml/i18n/replace_qtbase_de.ts b/src/pentobi_qml/qml/i18n/replace_qtbase_de.ts new file mode 100644 index 0000000..0fd0c68 --- /dev/null +++ b/src/pentobi_qml/qml/i18n/replace_qtbase_de.ts @@ -0,0 +1,11 @@ + + + + + QPlatformTheme + + Cancel + Abbrechen + + + diff --git a/src/pentobi_qml/qml/icons/menu.svg b/src/pentobi_qml/qml/icons/menu.svg new file mode 100644 index 0000000..6aed026 --- /dev/null +++ b/src/pentobi_qml/qml/icons/menu.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/icons/pentobi-backward.svg b/src/pentobi_qml/qml/icons/pentobi-backward.svg new file mode 100644 index 0000000..993c75d --- /dev/null +++ b/src/pentobi_qml/qml/icons/pentobi-backward.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/icons/pentobi-beginning.svg b/src/pentobi_qml/qml/icons/pentobi-beginning.svg new file mode 100644 index 0000000..baf6c04 --- /dev/null +++ b/src/pentobi_qml/qml/icons/pentobi-beginning.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/icons/pentobi-computer-colors.svg b/src/pentobi_qml/qml/icons/pentobi-computer-colors.svg new file mode 100644 index 0000000..8e7c890 --- /dev/null +++ b/src/pentobi_qml/qml/icons/pentobi-computer-colors.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/icons/pentobi-end.svg b/src/pentobi_qml/qml/icons/pentobi-end.svg new file mode 100644 index 0000000..7b4349e --- /dev/null +++ b/src/pentobi_qml/qml/icons/pentobi-end.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/icons/pentobi-forward.svg b/src/pentobi_qml/qml/icons/pentobi-forward.svg new file mode 100644 index 0000000..1fe5404 --- /dev/null +++ b/src/pentobi_qml/qml/icons/pentobi-forward.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/icons/pentobi-newgame.svg b/src/pentobi_qml/qml/icons/pentobi-newgame.svg new file mode 100644 index 0000000..202a885 --- /dev/null +++ b/src/pentobi_qml/qml/icons/pentobi-newgame.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/icons/pentobi-next-variation.svg b/src/pentobi_qml/qml/icons/pentobi-next-variation.svg new file mode 100644 index 0000000..5a75498 --- /dev/null +++ b/src/pentobi_qml/qml/icons/pentobi-next-variation.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/icons/pentobi-play.svg b/src/pentobi_qml/qml/icons/pentobi-play.svg new file mode 100644 index 0000000..e5441ce --- /dev/null +++ b/src/pentobi_qml/qml/icons/pentobi-play.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/icons/pentobi-previous-variation.svg b/src/pentobi_qml/qml/icons/pentobi-previous-variation.svg new file mode 100644 index 0000000..a58d536 --- /dev/null +++ b/src/pentobi_qml/qml/icons/pentobi-previous-variation.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/icons/pentobi-undo.svg b/src/pentobi_qml/qml/icons/pentobi-undo.svg new file mode 100644 index 0000000..1c23026 --- /dev/null +++ b/src/pentobi_qml/qml/icons/pentobi-undo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/dark/Theme.qml b/src/pentobi_qml/qml/themes/dark/Theme.qml new file mode 100644 index 0000000..edaac53 --- /dev/null +++ b/src/pentobi_qml/qml/themes/dark/Theme.qml @@ -0,0 +1,26 @@ +import QtQuick 2.0 + +QtObject { + property color backgroundColor: "#131313" + property color fontColorScore: "#C8C1BE" + property color fontColorPosInfo: "#C8C1BE" + property color colorBlue: "#0077D2" + property color colorYellow: "#EBCD23" + property color colorRed: "#E63E2C" + property color colorGreen: "#00C000" + property color colorStartingPoint: "#82777E" + property color backgroundButtonPressed: Qt.lighter(backgroundColor, 3) + property real pieceListOpacity: 0.94 + property real toPlayColorLighter: 1.7 + + function getImage(name) { + if (name.lastIndexOf("frame-", 0) === 0 + || name.lastIndexOf("junction-", 0) === 0 + || name.lastIndexOf("linesegment-", 0) === 0 + || name.lastIndexOf("piece-manipulator", 0) === 0 + || name.lastIndexOf("square-", 0) === 0 + || name.lastIndexOf("triangle-", 0) === 0) + return "themes/light/" + name + ".svg" + return "themes/dark/" + name + ".svg" + } +} diff --git a/src/pentobi_qml/qml/themes/dark/board-callisto-2.svg b/src/pentobi_qml/qml/themes/dark/board-callisto-2.svg new file mode 100644 index 0000000..dedd14c --- /dev/null +++ b/src/pentobi_qml/qml/themes/dark/board-callisto-2.svg @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/qml/themes/dark/board-callisto-3.svg b/src/pentobi_qml/qml/themes/dark/board-callisto-3.svg new file mode 100644 index 0000000..b97a7f9 --- /dev/null +++ b/src/pentobi_qml/qml/themes/dark/board-callisto-3.svg @@ -0,0 +1,232 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/qml/themes/dark/board-callisto.svg b/src/pentobi_qml/qml/themes/dark/board-callisto.svg new file mode 100644 index 0000000..17b22f3 --- /dev/null +++ b/src/pentobi_qml/qml/themes/dark/board-callisto.svg @@ -0,0 +1,300 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/qml/themes/dark/board-tile-classic.svg b/src/pentobi_qml/qml/themes/dark/board-tile-classic.svg new file mode 100644 index 0000000..a216929 --- /dev/null +++ b/src/pentobi_qml/qml/themes/dark/board-tile-classic.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/themes/dark/board-tile-nexos.svg b/src/pentobi_qml/qml/themes/dark/board-tile-nexos.svg new file mode 100644 index 0000000..6be4d0a --- /dev/null +++ b/src/pentobi_qml/qml/themes/dark/board-tile-nexos.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/pentobi_qml/qml/themes/dark/board-trigon-3.svg b/src/pentobi_qml/qml/themes/dark/board-trigon-3.svg new file mode 100644 index 0000000..0964fde --- /dev/null +++ b/src/pentobi_qml/qml/themes/dark/board-trigon-3.svg @@ -0,0 +1,394 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/qml/themes/dark/board-trigon.svg b/src/pentobi_qml/qml/themes/dark/board-trigon.svg new file mode 100644 index 0000000..ec0dbc3 --- /dev/null +++ b/src/pentobi_qml/qml/themes/dark/board-trigon.svg @@ -0,0 +1,496 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/Theme.qml b/src/pentobi_qml/qml/themes/light/Theme.qml new file mode 100644 index 0000000..0950ec6 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/Theme.qml @@ -0,0 +1,17 @@ +import QtQuick 2.0 + +QtObject { + property color backgroundColor: "#E6E5E5" + property color fontColorScore: "#5A5755" + property color fontColorPosInfo: "#282625" + property color colorBlue: "#0077D2" + property color colorYellow: "#EBCD23" + property color colorRed: "#E63E2C" + property color colorGreen: "#00C000" + property color colorStartingPoint: "#767074" + property color backgroundButtonPressed: Qt.lighter(backgroundColor) + property real pieceListOpacity: 1 + property real toPlayColorLighter: 0.5 + + function getImage(name) { return "themes/light/" + name + ".svg" } +} diff --git a/src/pentobi_qml/qml/themes/light/board-callisto-2.svg b/src/pentobi_qml/qml/themes/light/board-callisto-2.svg new file mode 100644 index 0000000..c7d2e5d --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/board-callisto-2.svg @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/board-callisto-3.svg b/src/pentobi_qml/qml/themes/light/board-callisto-3.svg new file mode 100644 index 0000000..bb0aeb3 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/board-callisto-3.svg @@ -0,0 +1,232 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/board-callisto.svg b/src/pentobi_qml/qml/themes/light/board-callisto.svg new file mode 100644 index 0000000..4338a7c --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/board-callisto.svg @@ -0,0 +1,300 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/board-tile-classic.svg b/src/pentobi_qml/qml/themes/light/board-tile-classic.svg new file mode 100644 index 0000000..31c0c82 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/board-tile-classic.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/board-tile-nexos.svg b/src/pentobi_qml/qml/themes/light/board-tile-nexos.svg new file mode 100644 index 0000000..7d5d4df --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/board-tile-nexos.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/board-trigon-3.svg b/src/pentobi_qml/qml/themes/light/board-trigon-3.svg new file mode 100644 index 0000000..5450dc1 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/board-trigon-3.svg @@ -0,0 +1,394 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/board-trigon.svg b/src/pentobi_qml/qml/themes/light/board-trigon.svg new file mode 100644 index 0000000..d7964e1 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/board-trigon.svg @@ -0,0 +1,496 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/frame-blue.svg b/src/pentobi_qml/qml/themes/light/frame-blue.svg new file mode 100644 index 0000000..b55e32e --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/frame-blue.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/frame-green.svg b/src/pentobi_qml/qml/themes/light/frame-green.svg new file mode 100644 index 0000000..dae34d9 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/frame-green.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/frame-red.svg b/src/pentobi_qml/qml/themes/light/frame-red.svg new file mode 100644 index 0000000..b73327b --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/frame-red.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/frame-yellow.svg b/src/pentobi_qml/qml/themes/light/frame-yellow.svg new file mode 100644 index 0000000..948a459 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/frame-yellow.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-all-blue.svg b/src/pentobi_qml/qml/themes/light/junction-all-blue.svg new file mode 100644 index 0000000..339e9d0 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-all-blue.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-all-green.svg b/src/pentobi_qml/qml/themes/light/junction-all-green.svg new file mode 100644 index 0000000..02f2de2 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-all-green.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-all-red.svg b/src/pentobi_qml/qml/themes/light/junction-all-red.svg new file mode 100644 index 0000000..6f7b77c --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-all-red.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-all-yellow.svg b/src/pentobi_qml/qml/themes/light/junction-all-yellow.svg new file mode 100644 index 0000000..52f3c58 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-all-yellow.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-rect-blue.svg b/src/pentobi_qml/qml/themes/light/junction-rect-blue.svg new file mode 100644 index 0000000..74dea2c --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-rect-blue.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-rect-green.svg b/src/pentobi_qml/qml/themes/light/junction-rect-green.svg new file mode 100644 index 0000000..6b88d65 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-rect-green.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-rect-red.svg b/src/pentobi_qml/qml/themes/light/junction-rect-red.svg new file mode 100644 index 0000000..fdd2958 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-rect-red.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-rect-yellow.svg b/src/pentobi_qml/qml/themes/light/junction-rect-yellow.svg new file mode 100644 index 0000000..d13407c --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-rect-yellow.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-straight-blue.svg b/src/pentobi_qml/qml/themes/light/junction-straight-blue.svg new file mode 100644 index 0000000..05fce46 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-straight-blue.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-straight-green.svg b/src/pentobi_qml/qml/themes/light/junction-straight-green.svg new file mode 100644 index 0000000..321ef53 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-straight-green.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-straight-red.svg b/src/pentobi_qml/qml/themes/light/junction-straight-red.svg new file mode 100644 index 0000000..78f4a3a --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-straight-red.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-straight-yellow.svg b/src/pentobi_qml/qml/themes/light/junction-straight-yellow.svg new file mode 100644 index 0000000..eb9a97c --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-straight-yellow.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-t-blue.svg b/src/pentobi_qml/qml/themes/light/junction-t-blue.svg new file mode 100644 index 0000000..3468515 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-t-blue.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-t-green.svg b/src/pentobi_qml/qml/themes/light/junction-t-green.svg new file mode 100644 index 0000000..1f36933 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-t-green.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-t-red.svg b/src/pentobi_qml/qml/themes/light/junction-t-red.svg new file mode 100644 index 0000000..b9a1367 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-t-red.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/junction-t-yellow.svg b/src/pentobi_qml/qml/themes/light/junction-t-yellow.svg new file mode 100644 index 0000000..cf431aa --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/junction-t-yellow.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi_qml/qml/themes/light/linesegment-blue.svg b/src/pentobi_qml/qml/themes/light/linesegment-blue.svg new file mode 100644 index 0000000..a4f9eb1 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/linesegment-blue.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/linesegment-green.svg b/src/pentobi_qml/qml/themes/light/linesegment-green.svg new file mode 100644 index 0000000..c8a0160 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/linesegment-green.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/linesegment-red.svg b/src/pentobi_qml/qml/themes/light/linesegment-red.svg new file mode 100644 index 0000000..102bee5 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/linesegment-red.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/linesegment-yellow.svg b/src/pentobi_qml/qml/themes/light/linesegment-yellow.svg new file mode 100644 index 0000000..548d2d8 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/linesegment-yellow.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/piece-manipulator-legal.svg b/src/pentobi_qml/qml/themes/light/piece-manipulator-legal.svg new file mode 100644 index 0000000..05928bb --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/piece-manipulator-legal.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/piece-manipulator.svg b/src/pentobi_qml/qml/themes/light/piece-manipulator.svg new file mode 100644 index 0000000..187e09e --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/piece-manipulator.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/square-blue.svg b/src/pentobi_qml/qml/themes/light/square-blue.svg new file mode 100644 index 0000000..63ed8aa --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/square-blue.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/square-green.svg b/src/pentobi_qml/qml/themes/light/square-green.svg new file mode 100644 index 0000000..916e69c --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/square-green.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/square-red.svg b/src/pentobi_qml/qml/themes/light/square-red.svg new file mode 100644 index 0000000..0e58a50 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/square-red.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/square-yellow.svg b/src/pentobi_qml/qml/themes/light/square-yellow.svg new file mode 100644 index 0000000..1989427 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/square-yellow.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/triangle-blue.svg b/src/pentobi_qml/qml/themes/light/triangle-blue.svg new file mode 100644 index 0000000..7a82407 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/triangle-blue.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/triangle-down-blue.svg b/src/pentobi_qml/qml/themes/light/triangle-down-blue.svg new file mode 100644 index 0000000..d93a7e0 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/triangle-down-blue.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/triangle-down-green.svg b/src/pentobi_qml/qml/themes/light/triangle-down-green.svg new file mode 100644 index 0000000..d848f64 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/triangle-down-green.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/triangle-down-red.svg b/src/pentobi_qml/qml/themes/light/triangle-down-red.svg new file mode 100644 index 0000000..1b5f55b --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/triangle-down-red.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/triangle-down-yellow.svg b/src/pentobi_qml/qml/themes/light/triangle-down-yellow.svg new file mode 100644 index 0000000..403f542 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/triangle-down-yellow.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/triangle-green.svg b/src/pentobi_qml/qml/themes/light/triangle-green.svg new file mode 100644 index 0000000..198b7bb --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/triangle-green.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/triangle-red.svg b/src/pentobi_qml/qml/themes/light/triangle-red.svg new file mode 100644 index 0000000..1afa8da --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/triangle-red.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/pentobi_qml/qml/themes/light/triangle-yellow.svg b/src/pentobi_qml/qml/themes/light/triangle-yellow.svg new file mode 100644 index 0000000..bb2c535 --- /dev/null +++ b/src/pentobi_qml/qml/themes/light/triangle-yellow.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/pentobi_qml/qml/themes/theme_dark.qrc b/src/pentobi_qml/qml/themes/theme_dark.qrc new file mode 100644 index 0000000..08609ea --- /dev/null +++ b/src/pentobi_qml/qml/themes/theme_dark.qrc @@ -0,0 +1,12 @@ + + +dark/board-callisto.svg +dark/board-callisto-2.svg +dark/board-callisto-3.svg +dark/board-tile-classic.svg +dark/board-tile-nexos.svg +dark/board-trigon.svg +dark/board-trigon-3.svg +dark/Theme.qml + + diff --git a/src/pentobi_qml/qml/themes/theme_light.qrc b/src/pentobi_qml/qml/themes/theme_light.qrc new file mode 100644 index 0000000..f4706d8 --- /dev/null +++ b/src/pentobi_qml/qml/themes/theme_light.qrc @@ -0,0 +1,12 @@ + + +light/board-callisto.svg +light/board-callisto-2.svg +light/board-callisto-3.svg +light/board-tile-classic.svg +light/board-tile-nexos.svg +light/board-trigon.svg +light/board-trigon-3.svg +light/Theme.qml + + diff --git a/src/pentobi_qml/qml/themes/theme_shared.qrc b/src/pentobi_qml/qml/themes/theme_shared.qrc new file mode 100644 index 0000000..63f183b --- /dev/null +++ b/src/pentobi_qml/qml/themes/theme_shared.qrc @@ -0,0 +1,42 @@ + + +light/frame-blue.svg +light/frame-green.svg +light/frame-red.svg +light/frame-yellow.svg +light/junction-all-blue.svg +light/junction-all-green.svg +light/junction-all-red.svg +light/junction-all-yellow.svg +light/junction-rect-blue.svg +light/junction-rect-green.svg +light/junction-rect-red.svg +light/junction-rect-yellow.svg +light/junction-straight-blue.svg +light/junction-straight-green.svg +light/junction-straight-red.svg +light/junction-straight-yellow.svg +light/junction-t-blue.svg +light/junction-t-green.svg +light/junction-t-red.svg +light/junction-t-yellow.svg +light/linesegment-blue.svg +light/linesegment-green.svg +light/linesegment-red.svg +light/linesegment-yellow.svg +light/piece-manipulator-legal.svg +light/piece-manipulator.svg +light/square-blue.svg +light/square-green.svg +light/square-red.svg +light/square-yellow.svg +light/triangle-blue.svg +light/triangle-down-blue.svg +light/triangle-down-green.svg +light/triangle-down-red.svg +light/triangle-down-yellow.svg +light/triangle-green.svg +light/triangle-red.svg +light/triangle-yellow.svg + + diff --git a/src/pentobi_qml/resources.qrc b/src/pentobi_qml/resources.qrc new file mode 100644 index 0000000..db9c32c --- /dev/null +++ b/src/pentobi_qml/resources.qrc @@ -0,0 +1,40 @@ + + + qml/AndroidToolBar.qml + qml/AndroidToolButton.qml + qml/Board.qml + qml/Button.qml + qml/ComputerColorDialog.qml + qml/GameDisplay.qml + qml/LineSegment.qml + qml/Main.qml + qml/MenuComputer.qml + qml/MenuEdit.qml + qml/MenuGame.qml + qml/MenuGo.qml + qml/MenuHelp.qml + qml/MenuItemGameVariant.qml + qml/MenuItemLevel.qml + qml/MenuView.qml + qml/NavigationPanel.qml + qml/OpenDialog.qml + qml/PieceCallisto.qml + qml/PieceClassic.qml + qml/PieceFlipAnimation.qml + qml/PieceList.qml + qml/PieceManipulator.qml + qml/PieceNexos.qml + qml/PieceRotationAnimation.qml + qml/PieceSelector.qml + qml/PieceTrigon.qml + qml/SaveDialog.qml + qml/ScoreDisplay.qml + qml/ScoreElement.qml + qml/ScoreElement2.qml + qml/Square.qml + qml/ToolBar.qml + qml/Triangle.qml + qml/Main.js + qml/GameDisplay.js + + diff --git a/src/pentobi_qml/translations.qrc b/src/pentobi_qml/translations.qrc new file mode 100644 index 0000000..9cc6b55 --- /dev/null +++ b/src/pentobi_qml/translations.qrc @@ -0,0 +1,6 @@ + + + qml/i18n/qml_de.qm + qml/i18n/replace_qtbase_de.qm + + diff --git a/src/pentobi_thumbnailer/CMakeLists.txt b/src/pentobi_thumbnailer/CMakeLists.txt new file mode 100644 index 0000000..43339b0 --- /dev/null +++ b/src/pentobi_thumbnailer/CMakeLists.txt @@ -0,0 +1,17 @@ +set(pentobi_thumbnailer_SRCS Main.cpp) + +add_executable(pentobi-thumbnailer Main.cpp) + +target_link_libraries(pentobi-thumbnailer + pentobi_thumbnail + pentobi_gui + pentobi_base + boardgame_base + boardgame_sgf + boardgame_util + boardgame_sys + ) + +target_link_libraries(pentobi-thumbnailer Qt5::Widgets) + +install(TARGETS pentobi-thumbnailer DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/src/pentobi_thumbnailer/Main.cpp b/src/pentobi_thumbnailer/Main.cpp new file mode 100644 index 0000000..c4f6e72 --- /dev/null +++ b/src/pentobi_thumbnailer/Main.cpp @@ -0,0 +1,55 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_thumbnailer/Main.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include +#include +#include +#include +#include +#include +#include "libpentobi_thumbnail/CreateThumbnail.h" + +using namespace std; + +//----------------------------------------------------------------------------- + +int main(int argc, char* argv[]) +{ + QCoreApplication app(argc, argv); + try + { + QCommandLineParser parser; + QCommandLineOption optionSize(QStringList() << "s" << "size", + "Generate image with height and width .", + "size", "128"); + parser.addOption(optionSize); + parser.process(app); + auto args = parser.positionalArguments(); + bool ok; + int size = parser.value(optionSize).toInt(&ok); + if (! ok || size <= 0) + throw runtime_error("Invalid image size"); + if (args.size() > 2) + throw runtime_error("Too many file arguments"); + if (args.size() < 2) + throw runtime_error("Need input and output file argument"); + QImage image(size, size, QImage::Format_ARGB32); + image.fill(Qt::transparent); + if (! createThumbnail(args.at(0), size, size, image)) + throw runtime_error("Thumbnail generation failed"); + QImageWriter writer(args.at(1), "png"); + if (! writer.write(image)) + throw runtime_error(writer.errorString().toLocal8Bit().constData()); + } + catch (const exception& e) + { + cerr << e.what() << '\n'; + return 1; + } + return 0; +} + +//----------------------------------------------------------------------------- diff --git a/src/twogtp/Analyze.cpp b/src/twogtp/Analyze.cpp new file mode 100644 index 0000000..4c1c974 --- /dev/null +++ b/src/twogtp/Analyze.cpp @@ -0,0 +1,173 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/Analyze.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Analyze.h" + +#include +#include +#include +#include "libboardgame_util/FmtSaver.h" +#include "libboardgame_util/Statistics.h" +#include "libboardgame_util/StringUtil.h" + +using libboardgame_util::from_string; +using libboardgame_util::split; +using libboardgame_util::trim; +using libboardgame_util::FmtSaver; +using libboardgame_util::Statistics; +using libboardgame_util::StatisticsExt; + +//----------------------------------------------------------------------------- + +namespace { + +void write_result(const Statistics<>& stat) +{ + FmtSaver saver(cout); + cout << fixed << setprecision(1) << stat.get_mean() * 100 << "+-" + << stat.get_error() * 100; +} + +} // namespace + +//----------------------------------------------------------------------------- + +void analyze(const string& file) +{ + ifstream in(file); + Statistics<> stat_result; + map> stat_result_player; + map result_count; + StatisticsExt<> stat_length; + StatisticsExt<> stat_cpu_b; + StatisticsExt<> stat_cpu_w; + StatisticsExt<> stat_fast_open; + string line; + while (getline(in, line)) + { + line = trim(line); + if (! line.empty() && line[0] == '#') + continue; + auto columns = split(line, '\t'); + if (columns.empty()) + continue; + float result; + unsigned length; + unsigned player; + float cpu_b; + float cpu_w; + unsigned fast_open; + if (columns.size() != 7 + || ! from_string(columns[1], result) + || ! from_string(columns[2], length) + || ! from_string(columns[3], player) + || ! from_string(columns[4], cpu_b) + || ! from_string(columns[5], cpu_w) + || ! from_string(columns[6], fast_open)) + throw runtime_error("invalid format"); + stat_result.add(result); + stat_result_player[player].add(result); + ++result_count[result]; + stat_length.add(length); + stat_cpu_b.add(cpu_b); + stat_cpu_w.add(cpu_w); + stat_fast_open.add(fast_open); + } + auto count = stat_result.get_count(); + cout << "Gam: " << count; + if (count == 0) + { + cout << '\n'; + return; + } + cout << ", Res: "; + write_result(stat_result); + cout << " ("; + bool is_first = true; + for (auto& i : stat_result_player) + { + if (! is_first) + cout << ", "; + else + is_first = false; + cout << i.first << ": "; + write_result(i.second); + } + cout << ")\nResFreq:"; + for (auto& i : result_count) + { + cout << ' ' << i.first << "="; + { + FmtSaver saver(cout); + auto fraction = i.second / count; + cout << fixed << setprecision(1) << fraction * 100 + << "+-" << sqrt(fraction * (1 - fraction) / count) * 100; + } + } + cout << "\nCpuB: "; + stat_cpu_b.write(cout, true, 3, false, true); + cout << "\nCpuW: "; + stat_cpu_w.write(cout, true, 3, false, true); + auto cpu_b = stat_cpu_b.get_mean(); + auto cpu_w = stat_cpu_w.get_mean(); + auto err_cpu_b = stat_cpu_b.get_error(); + auto err_cpu_w = stat_cpu_w.get_error(); + cout << "\nCpuB/CpuW: "; + if (cpu_b > 0 && cpu_w > 0) + cout << fixed << setprecision(3) << cpu_b / cpu_w << "+-" + << cpu_b / cpu_w * hypot(err_cpu_b / cpu_b, err_cpu_w / cpu_w); + else + cout << "-"; + cout << ", Len: "; + stat_length.write(cout, true, 1, true, true); + if (stat_fast_open.get_mean() > 0) + { + cout << ", Fast: "; + stat_fast_open.write(cout, true, 1, true, true); + } + cout << '\n'; +} + +void splitsgf(const string& file) +{ + ifstream in(file); + string filename; + string buffer; + regex pattern("GN\\[(\\d+)\\]"); + string line; + while (getline(in, line)) + { + if (trim(line) == "(") + { + if (! filename.empty()) + { + ofstream out(filename); + out << buffer; + } + buffer.clear(); + } + else + { + smatch match; + regex_search(line, match, pattern); + if (! match.empty()) + filename = string(match[1]) + ".blksgf"; + } + buffer.append(line); + buffer.append("\n"); + } + if (! filename.empty()) + { + ofstream out(filename); + out << buffer; + } +} + +//----------------------------------------------------------------------------- diff --git a/src/twogtp/Analyze.h b/src/twogtp/Analyze.h new file mode 100644 index 0000000..e1dc754 --- /dev/null +++ b/src/twogtp/Analyze.h @@ -0,0 +1,22 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/Analyze.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef TWOGTP_ANALYZE_H +#define TWOGTP_ANALYZE_H + +#include + +using namespace std; + +//----------------------------------------------------------------------------- + +void analyze(const string& file); + +void splitsgf(const string& file); + +//----------------------------------------------------------------------------- + +#endif // TWOGTP_ANALYZE_H diff --git a/src/twogtp/CMakeLists.txt b/src/twogtp/CMakeLists.txt new file mode 100644 index 0000000..a4dc5ec --- /dev/null +++ b/src/twogtp/CMakeLists.txt @@ -0,0 +1,28 @@ +add_executable(twogtp + Analyze.h + Analyze.cpp + FdStream.h + FdStream.cpp + GtpConnection.h + GtpConnection.cpp + Main.cpp + Output.h + Output.cpp + OutputTree.h + OutputTree.cpp + TwoGtp.h + TwoGtp.cpp +) + +target_link_libraries(twogtp + pentobi_base + boardgame_sgf + boardgame_base + boardgame_util + boardgame_sys +) + +if(CMAKE_THREAD_LIBS_INIT) + target_link_libraries(twogtp ${CMAKE_THREAD_LIBS_INIT}) +endif() + diff --git a/src/twogtp/FdStream.cpp b/src/twogtp/FdStream.cpp new file mode 100644 index 0000000..5ef5c81 --- /dev/null +++ b/src/twogtp/FdStream.cpp @@ -0,0 +1,93 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/FdStream.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "FdStream.h" + +#include +#include + +//----------------------------------------------------------------------------- + +namespace { + +const size_t put_back = 1; + +} // namespace + +//----------------------------------------------------------------------------- + +FdInBuf::FdInBuf(int fd, size_t buf_size) + : m_fd(fd), + m_buf(buf_size + put_back) +{ + auto end = &(*m_buf.end()); + setg(end, end, end); +} + +FdInBuf::~FdInBuf() = default; + +auto FdInBuf::underflow() -> int_type +{ + if (gptr() < egptr()) + return traits_type::to_int_type(*gptr()); + auto base = &m_buf.front(); + auto start = base; + if (eback() == base) + { + memmove(base, egptr() - put_back, put_back); + start += put_back; + } + auto n = read(m_fd, start, m_buf.size() - (start - base)); + if (n <= 0) + return traits_type::eof(); + setg(base, start, start + n); + return traits_type::to_int_type(*gptr()); +} + +//----------------------------------------------------------------------------- + +FdInStream::FdInStream(int fd) + : istream(nullptr), + m_buf(fd) +{ + rdbuf(&m_buf); +} + +//----------------------------------------------------------------------------- + +FdOutBuf::~FdOutBuf() = default; + +auto FdOutBuf::overflow(int_type c) -> int_type +{ + if (c != traits_type::eof()) + { + char buffer[1]; + buffer[0] = static_cast(c); + if (write(m_fd, buffer, 1) != 1) + return traits_type::eof(); + } + return c; +} + +streamsize FdOutBuf::xsputn(const char_type* s, streamsize count) +{ + return write(m_fd, s, count); +} + +//----------------------------------------------------------------------------- + +FdOutStream::FdOutStream(int fd) + : ostream(nullptr), + m_buf(fd) +{ + rdbuf(&m_buf); +} + +//----------------------------------------------------------------------------- diff --git a/src/twogtp/FdStream.h b/src/twogtp/FdStream.h new file mode 100644 index 0000000..25ea211 --- /dev/null +++ b/src/twogtp/FdStream.h @@ -0,0 +1,85 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/FdStream.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef TWOGTP_FDSTREAM_H +#define TWOGTP_FDSTREAM_H + +#include +#include + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Input stream buffer from a file descriptor. */ +class FdInBuf + : public streambuf +{ +public: + FdInBuf(int fd, size_t buf_size = 1024); + + ~FdInBuf(); + +protected: + int_type underflow() override; + +private: + int m_fd; + + vector m_buf; +}; + +//----------------------------------------------------------------------------- + +/** Input stream from a file descriptor. */ +class FdInStream + : public istream +{ +public: + explicit FdInStream(int fd); + +private: + FdInBuf m_buf; +}; + +//----------------------------------------------------------------------------- + +/** Output stream buffer from a file descriptor. */ +class FdOutBuf + : public streambuf +{ +public: + explicit FdOutBuf(int fd) + : m_fd(fd) + { } + + ~FdOutBuf(); + +protected: + int_type overflow(int_type c) override; + + streamsize xsputn(const char_type* s, streamsize count) override; + +private: + int m_fd; +}; + +//----------------------------------------------------------------------------- + +/** Output stream from a file descriptor. */ +class FdOutStream + : public ostream +{ +public: + explicit FdOutStream(int fd); + +private: + FdOutBuf m_buf; +}; + +//----------------------------------------------------------------------------- + +#endif // TWOGTP_FDSTREAM_H diff --git a/src/twogtp/GtpConnection.cpp b/src/twogtp/GtpConnection.cpp new file mode 100644 index 0000000..2873fc6 --- /dev/null +++ b/src/twogtp/GtpConnection.cpp @@ -0,0 +1,175 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/GtpConnection.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "GtpConnection.h" + +#include +#include +#include +#include +#include "FdStream.h" +#include "libboardgame_util/Log.h" + +//----------------------------------------------------------------------------- + +namespace { + +void terminate_child(const string& message) +{ + LIBBOARDGAME_LOG(message); + exit(1); +} + +vector split_args(string s) +{ + vector result; + bool escape = false; + bool is_in_string = false; + ostringstream token; + for (auto c : s) + { + if (c == '"' && ! escape) + { + if (is_in_string) + { + result.push_back(token.str()); + token.str(""); + } + is_in_string = ! is_in_string; + } + else if (isspace(c) && ! is_in_string) + { + if (! token.str().empty()) + { + result.push_back(token.str()); + token.str(""); + } + } + else + token << c; + escape = (c == '\\' && ! escape); + } + if (! token.str().empty()) + result.push_back(token.str()); + return result; +} + +} // namespace + +//----------------------------------------------------------------------------- + +GtpConnection::GtpConnection(const string& command) +{ + vector args = split_args(command); + if (args.size() == 0) + throw runtime_error("GtpConnection: empty command line"); + int fd1[2]; + if (pipe(fd1) < 0) + throw runtime_error("GtpConnection: pipe creation failed"); + int fd2[2]; + if (pipe(fd2) < 0) + { + close(fd1[0]); + close(fd1[1]); + throw runtime_error("GtpConnection: pipe creation failed"); + } + pid_t pid; + if ((pid = fork()) < 0) + throw runtime_error("GtpConnection: fork failed"); + else if (pid > 0) // Parent + { + close(fd1[0]); + close(fd2[1]); + m_in.reset(new FdInStream(fd2[0])); + m_out.reset(new FdOutStream(fd1[1])); + return; + } + else // Child + { + close(fd1[1]); + close(fd2[0]); + if (fd1[0] != STDIN_FILENO) + if (dup2(fd1[0], STDIN_FILENO) != STDIN_FILENO) + { + close(fd1[0]); + terminate_child("GtpConnection: dup2 to stdin failed"); + } + if (fd2[1] != STDOUT_FILENO) + if (dup2(fd2[1], STDOUT_FILENO) != STDOUT_FILENO) + { + close(fd2[1]); + terminate_child("GtpConnection: dup2 to stdout failed"); + } + auto const argv = new char*[args.size() + 1]; + for (size_t i = 0; i < args.size(); ++i) + { + argv[i] = new char[args[i].size() + 1]; + strcpy(argv[i], args[i].c_str()); + } + argv[args.size()] = nullptr; + if (execvp(args[0].c_str(), argv) == -1) + terminate_child("Could not execute '" + command + "'"); + } +} + +GtpConnection::~GtpConnection() = default; + +void GtpConnection::enable_log(const string& prefix) +{ + m_quiet = false; + m_prefix = prefix; +} + +string GtpConnection::send(const string& command) +{ + if (! m_quiet) + LIBBOARDGAME_LOG(m_prefix, ">> ", command); + *m_out << command << '\n'; + m_out->flush(); + if (! *m_out) + throw Failure("GtpConnection: write failure"); + ostringstream response; + bool done = false; + bool is_first = true; + bool success = true; + while (! done) + { + string line; + getline(*m_in, line); + if (! *m_in) + throw Failure("GtpConnection: read failure"); + if (! m_quiet && ! line.empty()) + LIBBOARDGAME_LOG(m_prefix, "<< ", line); + if (is_first) + { + if (line.size() < 2 || (line[0] != '=' && line[0] != '?') + || line[1] != ' ') + throw Failure("GtpConnection: malformed response: '" + line + + "'"); + if (line[0] == '?') + success = false; + line = line.substr(2); + response << line; + is_first = false; + } + else + { + if (line.empty()) + done = true; + else + response << '\n' << line; + } + } + if (! success) + throw Failure(response.str()); + return response.str(); +} + +//----------------------------------------------------------------------------- diff --git a/src/twogtp/GtpConnection.h b/src/twogtp/GtpConnection.h new file mode 100644 index 0000000..3c21150 --- /dev/null +++ b/src/twogtp/GtpConnection.h @@ -0,0 +1,53 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/GtpConnection.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef TWOGTP_GTP_CONNECTION_H +#define TWOGTP_GTP_CONNECTION_H + +#include +#include +#include + +using namespace std; + +//----------------------------------------------------------------------------- + +/** Invokes a GTP engine in an external process. */ +class GtpConnection +{ +public: + class Failure + : public runtime_error + { + using runtime_error::runtime_error; + }; + + + explicit GtpConnection(const string& command); + + ~GtpConnection(); + + void enable_log(const string& prefix = ""); + + /** Send a GTP command. + @param command The command. + @return The response if the command returns a success status. + @throws Failure If the command returns an error status. */ + string send(const string& command); + +private: + bool m_quiet = true; + + string m_prefix; + + unique_ptr m_in; + + unique_ptr m_out; +}; + +//----------------------------------------------------------------------------- + +#endif // TWOGTP_GTP_CONNECTION_H diff --git a/src/twogtp/Main.cpp b/src/twogtp/Main.cpp new file mode 100644 index 0000000..ea90942 --- /dev/null +++ b/src/twogtp/Main.cpp @@ -0,0 +1,105 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/Main.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include "Analyze.h" +#include "TwoGtp.h" +#include "libboardgame_util/Log.h" +#include "libboardgame_util/Options.h" +#include "libpentobi_base/Variant.h" + +using namespace std; +using libboardgame_util::to_string; +using libboardgame_util::Options; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +int main(int argc, char** argv) +{ + atomic result(0); + try + { + vector specs = { + "analyze:", + "black|b:", + "fastopen", + "file|f:", + "game|g:", + "nugames|n:", + "quiet", + "splitsgf:", + "threads:", + "tree", + "white|w:", + }; + Options opt(argc, argv, specs); + if (opt.contains("analyze")) + { + analyze(opt.get("analyze")); + return 0; + } + if (opt.contains("splitsgf")) + { + splitsgf(opt.get("splitsgf")); + return 0; + } + auto black = opt.get("black"); + auto white = opt.get("white"); + auto prefix = opt.get("file", "output"); + auto nu_games = opt.get("nugames", 1); + auto nu_threads = opt.get("threads", 1); + auto variant_string = opt.get("game", "classic"); + bool quiet = opt.contains("quiet"); + if (quiet) + libboardgame_util::disable_logging(); + bool fast_open = opt.contains("fastopen"); + bool create_tree = opt.contains("tree") || fast_open; + Variant variant; + if (! parse_variant_id(variant_string, variant)) + throw runtime_error("invalid game variant " + variant_string); + Output output(variant, prefix, create_tree); + vector> twogtp; + for (unsigned i = 0; i < nu_threads; ++i) + { + string log_prefix; + if (nu_threads > 1) + log_prefix = to_string(i + 1); + twogtp.push_back(make_shared(black, white, variant, + nu_games, output, quiet, + log_prefix, fast_open)); + } + vector threads; + for (auto& i : twogtp) + threads.push_back(thread([&i, &result]() + { + try + { + i->run(); + } + catch (const exception& e) + { + LIBBOARDGAME_LOG("Error: ", e.what()); + result = 1; + } + })); + for (auto& t : threads) + t.join(); + } + catch (const exception& e) + { + LIBBOARDGAME_LOG("Error: ", e.what()); + result = 1; + } + return result; +} + +//----------------------------------------------------------------------------- diff --git a/src/twogtp/Output.cpp b/src/twogtp/Output.cpp new file mode 100644 index 0000000..e56d16a --- /dev/null +++ b/src/twogtp/Output.cpp @@ -0,0 +1,128 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/Output.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "Output.h" + +#include +#include +#include +#include +#include +#include +#include "libboardgame_util/StringUtil.h" + +using libboardgame_util::from_string; +using libboardgame_util::split; +using libboardgame_util::trim; + +//----------------------------------------------------------------------------- + +Output::Output(Variant variant, const string& prefix, bool create_tree) + : m_create_tree(create_tree), + m_prefix(prefix), + m_output_tree(variant) +{ + m_lock_fd = creat((prefix + ".lock").c_str(), 0644); + if (m_lock_fd == -1) + throw runtime_error("Output: could not create lock file"); + if (flock(m_lock_fd, LOCK_EX | LOCK_NB) == -1) + throw runtime_error("Output: twogtp already running"); + ifstream in(prefix + ".dat"); + if (! in) + return; + string line; + while (getline(in, line)) + { + line = trim(line); + if (! line.empty() && line[0] == '#') + continue; + auto columns = split(line, '\t'); + if (columns.empty()) + continue; + unsigned game_number; + if (! from_string(columns[0], game_number)) + throw runtime_error("Output: expected game number"); + m_games.insert(make_pair(game_number, line)); + } + while (m_games.count(m_next) != 0) + ++m_next; + if (check_sentinel()) + remove((prefix + ".stop").c_str()); + if (m_create_tree && m_next > 0) + m_output_tree.load(prefix + "-tree.blksgf"); +} + +Output::~Output() +{ + flock(m_lock_fd, LOCK_UN); + close(m_lock_fd); + remove((m_prefix + ".lock").c_str()); +} + +void Output::add_result(unsigned n, float result, const Board& bd, + unsigned player_black, double cpu_black, + double cpu_white, const string& sgf, + const array& is_real_move) +{ + lock_guard lock(m_mutex); + unsigned nu_fast_open = 0; + for (unsigned i = 0; i < bd.get_nu_moves(); ++i) + if (! is_real_move[i]) + ++nu_fast_open; + ostringstream line; + line << n << '\t' + << setprecision(4) << result << '\t' + << bd.get_nu_moves() << '\t' + << player_black << '\t' + << setprecision(5) << cpu_black << '\t' + << cpu_white << '\t' + << nu_fast_open; + m_games.insert(make_pair(n, line.str())); + { + ofstream out(m_prefix + ".dat"); + out << "# Game\tResult\tLength\tPlayerB\tCpuB\tCpuW\tFast\n"; + for (auto& i : m_games) + out << i.second << '\n'; + } + { + ofstream out(m_prefix + ".blksgf", ios::app); + out << sgf; + } + if (m_create_tree) + { + m_output_tree.add_game(bd, player_black, result, is_real_move); + m_output_tree.save(m_prefix + "-tree.blksgf"); + } +} + +bool Output::check_sentinel() +{ + return ! ifstream(m_prefix + ".stop").fail(); +} + +bool Output::generate_fast_open_move(bool is_player_black, const Board& bd, + Color to_play, Move& mv) +{ + lock_guard lock(m_mutex); + m_output_tree.generate_move(is_player_black, bd, to_play, mv); + return ! mv.is_null(); +} + +unsigned Output::get_next() +{ + lock_guard lock(m_mutex); + unsigned n = m_next; + do + ++m_next; + while (m_games.count(m_next) != 0); + return n; +} + +//----------------------------------------------------------------------------- diff --git a/src/twogtp/Output.h b/src/twogtp/Output.h new file mode 100644 index 0000000..7dc2c7f --- /dev/null +++ b/src/twogtp/Output.h @@ -0,0 +1,55 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/Output.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef TWOGTP_OUTPUT_H +#define TWOGTP_OUTPUT_H + +#include +#include +#include +#include "OutputTree.h" + +//----------------------------------------------------------------------------- + +/** Handles the output files of TwoGtp and their concurrent access. */ +class Output +{ +public: + Output(Variant variant, const string& prefix, bool fastopen); + + ~Output(); + + void add_result(unsigned n, float result, const Board& bd, + unsigned player_black, double cpu_black, double cpu_white, + const string& sgf, + const array& is_real_move); + + unsigned get_next(); + + bool check_sentinel(); + + bool generate_fast_open_move(bool is_player_black, const Board& bd, + Color to_play, Move& mv); + +private: + bool m_create_tree; + + unsigned m_next = 0; + + int m_lock_fd; + + string m_prefix; + + mutex m_mutex; + + map m_games; + + OutputTree m_output_tree; +}; + +//----------------------------------------------------------------------------- + +#endif // TWOGTP_OUTPUT_H diff --git a/src/twogtp/OutputTree.cpp b/src/twogtp/OutputTree.cpp new file mode 100644 index 0000000..0898fd6 --- /dev/null +++ b/src/twogtp/OutputTree.cpp @@ -0,0 +1,241 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/OutputTree.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "OutputTree.h" + +#include +#include "libboardgame_sgf/TreeReader.h" +#include "libboardgame_sgf/TreeWriter.h" +#include "libpentobi_base/BoardUtil.h" + +using libboardgame_sgf::SgfNode; +using libboardgame_sgf::TreeReader; +using libboardgame_sgf::TreeWriter; +using libpentobi_base::get_transforms; +using libpentobi_base::ColorMove; +using libpentobi_base::MovePoints; +using libpentobi_base::boardutil::get_transformed; + +//----------------------------------------------------------------------------- + +namespace { + +void add(PentobiTree& tree, const SgfNode& node, bool is_player_black, + bool is_real_move, float result) +{ + unsigned index = is_player_black ? 0 : 1; + array count; + array avg_result; + array real_count; + auto comment = tree.get_comment(node); + if (comment.empty()) + { + count.fill(0); + avg_result.fill(0); + real_count.fill(0); + count[index] = 1; + real_count[index] = 1; + avg_result[index] = result; + } + else + { + istringstream in(comment); + in >> count[0] >> real_count[0] >> avg_result[0] + >> count[1] >> real_count[1] >> avg_result[1]; + if (! in) + throw runtime_error("OutputTree: invalid comment: " + comment); + ++count[index]; + avg_result[index] += (result - avg_result[index]) / count[index]; + if (is_real_move) + ++real_count[index]; + } + ostringstream out; + out.precision(numeric_limits::digits10); + out << count[0] << ' ' << real_count[0] << ' ' << avg_result[0] << '\n' + << count[1] << ' ' << real_count[1] << ' ' << avg_result[1]; + tree.set_comment(node, out.str()); +} + +bool compare_sequence(ArrayList& s1, + ArrayList& s2) +{ + LIBBOARDGAME_ASSERT(s1.size() == s2.size()); + for (unsigned i = 0; i < s1.size(); ++i) + { + LIBBOARDGAME_ASSERT(s1[i].color == s2[i].color); + if (s1[i].move.to_int() < s2[i].move.to_int()) + return true; + else if (s1[i].move.to_int() > s2[i].move.to_int()) + return false; + } + return false; +} + +unsigned get_real_count(PentobiTree& tree, const SgfNode& node, + bool is_player_black) +{ + unsigned index = is_player_black ? 0 : 1; + array count; + array avg_result; + array real_count; + auto comment = tree.get_comment(node); + istringstream in(comment); + in >> count[0] >> real_count[0] >> avg_result[0] + >> count[1] >> real_count[1] >> avg_result[1]; + if (! in) + throw runtime_error("OutputTree: invalid comment: " + comment); + return real_count[index]; +} + +} // namespace + +//----------------------------------------------------------------------------- + +OutputTree::OutputTree(Variant variant) + : m_tree(variant) +{ + get_transforms(variant, m_transforms, m_inv_transforms); +} + +OutputTree::~OutputTree() +{ +} + +void OutputTree::add_game(const Board& bd, unsigned player_black, float result, + const array& is_real_move) +{ + if (bd.has_setup()) + throw runtime_error("OutputTree: setup not supported"); + + // Find the canonical representation + ArrayList sequence; + for (auto& transform : m_transforms) + { + ArrayList s; + for (unsigned i = 0; i < bd.get_nu_moves(); ++i) + { + auto mv = bd.get_move(i); + s.push_back(ColorMove(mv.color, + get_transformed(bd, mv.move, *transform))); + } + if (sequence.empty() || compare_sequence(s, sequence)) + sequence = s; + } + + auto node = &m_tree.get_root(); + add(m_tree, *node, player_black == 0, true, result); + unsigned nu_moves_3 = 0; + for (unsigned i = 0; i < sequence.size(); ++i) + { + unsigned player; + auto mv = sequence[i]; + Color c = mv.color; + if (bd.get_variant() == Variant::classic_3 && c == Color(3)) + { + player = nu_moves_3 % 3; + ++nu_moves_3; + } + else + player = c.to_int() % bd.get_nu_players(); + auto child = m_tree.find_child_with_move(*node, mv); + if (! child) + { + child = &m_tree.create_new_child(*node); + m_tree.set_move(*child, mv); + add(m_tree, *child, player == player_black, true, result); + return; + } + add(m_tree, *child, player == player_black, is_real_move[i], result); + node = child; + } +} + +void OutputTree::generate_move(bool is_player_black, const Board& bd, + Color to_play, Move& mv) +{ + bool play_real; + for (unsigned i = 0; i < m_transforms.size(); ++i) + { + generate_move(is_player_black, bd, to_play, *m_transforms[i], + *m_inv_transforms[i], mv, play_real); + if (play_real || ! mv.is_null()) + break; + } +} + +void OutputTree::generate_move(bool is_player_black, const Board& bd, + Color to_play, const PointTransform& transform, + const PointTransform& inv_transform, Move& mv, + bool& play_real) +{ + if (bd.has_setup()) + throw runtime_error("OutputTree: setup not supported"); + play_real = false; + mv = Move::null(); + auto node = &m_tree.get_root(); + for (unsigned i = 0; i < bd.get_nu_moves(); ++i) + { + auto mv = bd.get_move(i); + ColorMove transformed_mv(mv.color, + get_transformed(bd, mv.move, transform)); + auto child = m_tree.find_child_with_move(*node, transformed_mv); + if (! child) + return; + node = child; + } + unsigned sum = 0; + for (auto& i : node->get_children()) + sum += get_real_count(m_tree, i, is_player_black); + if (sum == 0) + return; + uniform_real_distribution distribution(0, 1); + if (distribution(m_random) < 1.0 / sum) + { + play_real = true; + return; + } + unsigned random = static_cast(distribution(m_random) * sum); + sum = 0; + for (auto& i : node->get_children()) + { + auto real_count = get_real_count(m_tree, i, is_player_black); + if (real_count == 0) + continue; + sum += real_count; + if (sum >= random) + { + auto color_mv = m_tree.get_move(i); + if (color_mv.is_null()) + throw runtime_error("OutputTree: tree has node without move"); + if (color_mv.color != to_play) + throw runtime_error("OutputTree: tree has node wrong move color"); + mv = get_transformed(bd, color_mv.move, inv_transform); + return; + } + } + LIBBOARDGAME_ASSERT(false); +} + +void OutputTree::load(const string& file) +{ + TreeReader reader; + reader.read(file); + auto tree = reader.get_tree_transfer_ownership(); + m_tree.init(tree); +} + +void OutputTree::save(const string& file) +{ + ofstream out(file); + TreeWriter writer(out, m_tree.get_root()); + writer.write(); +} + +//----------------------------------------------------------------------------- diff --git a/src/twogtp/OutputTree.h b/src/twogtp/OutputTree.h new file mode 100644 index 0000000..9211f8d --- /dev/null +++ b/src/twogtp/OutputTree.h @@ -0,0 +1,82 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/OutputTree.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef TWOGTP_OUTPUT_TREE_H +#define TWOGTP_OUTPUT_TREE_H + +#include +#include "libpentobi_base/Board.h" +#include "libpentobi_base/PentobiTree.h" + +using namespace std; +using libboardgame_base::PointTransform; +using libboardgame_util::ArrayList; +using libpentobi_base::Board; +using libpentobi_base::Color; +using libpentobi_base::Move; +using libpentobi_base::PentobiTree; +using libpentobi_base::Point; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +/** Merges opening moves played by the players into a tree. + + Keeps statistics of the average game result for each move and player. + This class can also speed up playing test games by generating opening moves + according to the measured probability distributions. With some probabilty, + which decreases with the number of times a position was visited but stays + non-zero, the player generates a real move, which is used to update the + distributions, otherwise a move from the tree is played. In the limit, the + player plays an infinite number of real moves in each position, so the + measured distributions approach the real distributions and the result of + the test games approaches the result as if only real moves had been + played. */ +class OutputTree +{ +public: + explicit OutputTree(Variant variant); + + ~OutputTree(); + + void load(const string& file); + + void save(const string& file); + + /** Generate a move for a player from the tree. + @param is_player_black + @param bd The board with the current position. + @param to_play The color to generate the move for.. + @param[out] mv The generated move, or Move::null() if no move is in the + tree for this position or if the player should generate a real move + now. */ + void generate_move(bool is_player_black, const Board& bd, Color to_play, + Move& mv); + + /** Add the moves of a game to the tree and update the move counters. */ + void add_game(const Board& bd, unsigned player_black, float result, + const array& is_real_move); + +private: + typedef libboardgame_base::PointTransform PointTransform; + + PentobiTree m_tree; + + vector> m_transforms; + + vector> m_inv_transforms; + + mt19937 m_random; + + void generate_move(bool is_player_black, const Board& bd, Color to_play, + const PointTransform& transform, + const PointTransform& inv_transform, Move& mv, + bool& play_real); +}; + +//----------------------------------------------------------------------------- + +#endif // TWOGTP_OUTPUT_TREE_H diff --git a/src/twogtp/TwoGtp.cpp b/src/twogtp/TwoGtp.cpp new file mode 100644 index 0000000..ced86db --- /dev/null +++ b/src/twogtp/TwoGtp.cpp @@ -0,0 +1,195 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/TwoGtp.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "TwoGtp.h" + +#include "libboardgame_sgf/Writer.h" +#include "libboardgame_util/Log.h" +#include "libboardgame_util/StringUtil.h" +#include "libpentobi_base/ScoreUtil.h" + +using libboardgame_sgf::Writer; +using libboardgame_util::trim; +using libpentobi_base::get_multiplayer_result; +using libpentobi_base::Move; +using libpentobi_base::PieceSet; +using libpentobi_base::ScoreType; + +//----------------------------------------------------------------------------- + +TwoGtp::TwoGtp(const string& black, const string& white, Variant variant, + unsigned nu_games, Output& output, bool quiet, + const string& log_prefix, bool fast_open) + : m_quiet(quiet), + m_fast_open(fast_open), + m_variant(variant), + m_nu_games(nu_games), + m_bd(variant), + m_output(output), + m_black(black), + m_white(white) +{ + if (! m_quiet) + { + m_black.enable_log(log_prefix + "B"); + m_white.enable_log(log_prefix + "W"); + } + if (get_nu_colors(m_variant) == 2) + { + m_colors[0] = "b"; + m_colors[1] = "w"; + } + else + { + m_colors[0] = "1"; + m_colors[1] = "2"; + m_colors[2] = "3"; + m_colors[3] = "4"; + } +} + +float TwoGtp::get_result(unsigned player_black) +{ + float result; + auto nu_players = m_bd.get_nu_players(); + if (nu_players == 2) + { + auto score = m_bd.get_score_twoplayer(Color(0)); + if (score > 0) + result = 1; + else if (score < 0) + result = 0; + else + result = 0.5; + if (player_black != 0) + result = 1 - result; + } + else + { + array points; + for (Color::IntType i = 0; i < m_bd.get_nu_colors(); ++i) + points[i] = m_bd.get_points(Color(i)); + array player_result; + bool break_ties = (m_bd.get_piece_set() == PieceSet::callisto); + get_multiplayer_result(nu_players, points, player_result, break_ties); + result = player_result[player_black]; + } + return result; +} + +void TwoGtp::play_game(unsigned game_number) +{ + if (! m_quiet) + LIBBOARDGAME_LOG("================================================\n" + "Game ", game_number, "\n" + "================================================"); + m_bd.init(); + send_both("clear_board"); + auto cpu_black = send_cputime(m_black); + auto cpu_white = send_cputime(m_white); + unsigned nu_players = m_bd.get_nu_players(); + unsigned player_black = game_number % nu_players; + bool resign = false; + ostringstream sgf_string; + Writer sgf(sgf_string); + sgf.set_indent(0); + sgf.begin_tree(); + sgf.begin_node(); + sgf.write_property("GM", to_string(m_variant)); + sgf.write_property("GN", game_number); + sgf.end_node(); + array is_real_move; + unsigned player; + while (! m_bd.is_game_over()) + { + auto to_play = m_bd.get_effective_to_play(); + if (m_variant == Variant::classic_3 && to_play == Color(3)) + player = m_bd.get_alt_player(); + else + player = to_play.to_int() % nu_players; + auto& player_connection = (player == player_black ? m_black : m_white); + auto& other_connection = (player == player_black ? m_white : m_black); + auto color = m_colors[to_play.to_int()]; + Move mv; + if (m_fast_open + && m_output.generate_fast_open_move(player == player_black, + m_bd, to_play, mv)) + { + is_real_move[m_bd.get_nu_moves()] = false; + LIBBOARDGAME_LOG("Playing fast opening move"); + player_connection.send("play " + color + " " + m_bd.to_string(mv)); + } + else + { + is_real_move[m_bd.get_nu_moves()] = true; + auto response = player_connection.send("genmove " + color); + if (response == "resign") + { + resign = true; + break; + } + mv = m_bd.from_string(response); + } + sgf.begin_node(); + sgf.write_property(string(1, static_cast(toupper(color[0]))), + m_bd.to_string(mv)); + sgf.end_node(); + if (mv.is_null() || ! m_bd.is_legal(to_play, mv)) + throw runtime_error("invalid move: " + m_bd.to_string(mv)); + m_bd.play(to_play, mv); + other_connection.send("play " + color + " " + m_bd.to_string(mv)); + } + cpu_black = send_cputime(m_black) - cpu_black; + cpu_white = send_cputime(m_white) - cpu_white; + float result; + if (resign) + { + if (nu_players > 2) + throw runtime_error("resign only allowed in two-player variants"); + result = (player == player_black ? 0 : 1); + } + else + result = get_result(player_black); + sgf.end_tree(); + m_output.add_result(game_number, result, m_bd, player_black, cpu_black, + cpu_white, sgf_string.str(), is_real_move); +} + +void TwoGtp::run() +{ + send_both(string("set_game ") + to_string(m_variant)); + while (! m_output.check_sentinel()) + { + unsigned n = m_output.get_next(); + if (n >= m_nu_games) + break; + play_game(n); + } + send_both("quit"); +} + +void TwoGtp::send_both(const string& cmd) +{ + m_black.send(cmd); + m_white.send(cmd); +} + +double TwoGtp::send_cputime(GtpConnection& gtp_connection) +{ + string response = gtp_connection.send("cputime"); + istringstream in(response); + double cputime; + in >> cputime; + if (! in) + throw runtime_error("invalid response to cputime: " + response); + return cputime; +} + +//----------------------------------------------------------------------------- diff --git a/src/twogtp/TwoGtp.h b/src/twogtp/TwoGtp.h new file mode 100644 index 0000000..59116f9 --- /dev/null +++ b/src/twogtp/TwoGtp.h @@ -0,0 +1,61 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/TwoGtp.h + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifndef TWOGTP_TWOGTP_H +#define TWOGTP_TWOGTP_H + +#include +#include "GtpConnection.h" +#include "Output.h" +#include "libpentobi_base/Board.h" + +using namespace std; +using libpentobi_base::Board; +using libpentobi_base::Color; +using libpentobi_base::Variant; + +//----------------------------------------------------------------------------- + +class TwoGtp +{ +public: + TwoGtp(const string& black, const string& white, Variant variant, + unsigned nu_games, Output& output, bool quiet, + const string& log_prefix, bool fast_open); + + void run(); + +private: + bool m_quiet; + + bool m_fast_open; + + Variant m_variant; + + unsigned m_nu_games; + + Board m_bd; + + Output& m_output; + + GtpConnection m_black; + + GtpConnection m_white; + + array m_colors; + + float get_result(unsigned player_black); + + void play_game(unsigned game_number); + + void send_both(const string& cmd); + + double send_cputime(GtpConnection& gtp_connection); +}; + +//----------------------------------------------------------------------------- + +#endif // TWOGTP_TWOGTP_H diff --git a/src/unittest/CMakeLists.txt b/src/unittest/CMakeLists.txt new file mode 100644 index 0000000..88fb251 --- /dev/null +++ b/src/unittest/CMakeLists.txt @@ -0,0 +1,10 @@ +add_subdirectory(libboardgame_util) +add_subdirectory(libboardgame_sgf) +add_subdirectory(libboardgame_base) +add_subdirectory(libboardgame_mcts) +add_subdirectory(libpentobi_base) +add_subdirectory(libpentobi_mcts) + +if (PENTOBI_BUILD_GTP) + add_subdirectory(libboardgame_gtp) +endif() diff --git a/src/unittest/libboardgame_base/CMakeLists.txt b/src/unittest/libboardgame_base/CMakeLists.txt new file mode 100644 index 0000000..8ef1b1d --- /dev/null +++ b/src/unittest/libboardgame_base/CMakeLists.txt @@ -0,0 +1,17 @@ +add_executable(unittest_libboardgame_base + MarkerTest.cpp + PointTransformTest.cpp + RatingTest.cpp + RectGeometryTest.cpp + StringRepTest.cpp +) + +target_link_libraries(unittest_libboardgame_base + boardgame_test_main + boardgame_base + boardgame_test + boardgame_util + boardgame_sys + ) + +add_test(libboardgame_base unittest_libboardgame_base) diff --git a/src/unittest/libboardgame_base/MarkerTest.cpp b/src/unittest/libboardgame_base/MarkerTest.cpp new file mode 100644 index 0000000..255d89a --- /dev/null +++ b/src/unittest/libboardgame_base/MarkerTest.cpp @@ -0,0 +1,61 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_base/MarkerTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_base/Marker.h" +#include "libboardgame_base/Point.h" +#include "libboardgame_test/Test.h" + +using namespace std; + +//----------------------------------------------------------------------------- + +typedef libboardgame_base::Point<19 * 19, 19, 19, unsigned short> Point; +typedef libboardgame_base::Marker Marker; + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(boardgame_marker_basic) +{ + Marker m; + Point p1(10); + Point p2(11); + LIBBOARDGAME_CHECK(! m.set(p1)); + LIBBOARDGAME_CHECK(! m.set(p2)); + LIBBOARDGAME_CHECK(m.set(p1)); + LIBBOARDGAME_CHECK(m.set(p2)); + m.clear(); + LIBBOARDGAME_CHECK(! m.set(p1)); + LIBBOARDGAME_CHECK(! m.set(p2)); +} + +/** Test clear after a number of clears around the maximum unsigned integer + value. + This is a critical point of the implementation, which assumes that + values not equal to a clear counter are unmarked and the overflow of the + clear counter must be handled correctly. + This test is only run, if integers are not larger than 32-bit, otherwise + it would take too long. */ +LIBBOARDGAME_TEST_CASE(boardgame_marker_overflow) +{ + if (numeric_limits::digits > 32) + return; + Marker m; + m.setup_for_overflow_test(numeric_limits::max() - 5); + Point p1(10); + Point p2(11); + for (int i = 0; i < 10; ++i) + { + LIBBOARDGAME_CHECK(! m.set(p1)); + LIBBOARDGAME_CHECK(! m.set(p2)); + m.clear(); + } +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_base/PointTransformTest.cpp b/src/unittest/libboardgame_base/PointTransformTest.cpp new file mode 100644 index 0000000..2731b1e --- /dev/null +++ b/src/unittest/libboardgame_base/PointTransformTest.cpp @@ -0,0 +1,46 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_base/PointTransformTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_base/Point.h" +#include "libboardgame_base/PointTransform.h" +#include "libboardgame_base/RectGeometry.h" +#include "libboardgame_test/Test.h" + +using namespace std; + +//----------------------------------------------------------------------------- + +typedef libboardgame_base::Point<19 * 19, 19, 19, unsigned short> Point; +typedef libboardgame_base::RectGeometry RectGeometry; + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(boardgame_point_transform_get_transformed) +{ + unsigned sz = 9; + auto& geo = RectGeometry::get(sz, sz); + Point p = geo.get_point(1, 2); + { + libboardgame_base::PointTransfIdent transform; + LIBBOARDGAME_CHECK(transform.get_transformed(p, geo) == p); + } + { + libboardgame_base::PointTransfRot180 transform; + LIBBOARDGAME_CHECK(transform.get_transformed(p, geo) + == geo.get_point(7, 6)); + } + { + libboardgame_base::PointTransfRot270Refl transform; + LIBBOARDGAME_CHECK(transform.get_transformed(p, geo) + == geo.get_point(2, 1)); + } +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_base/RatingTest.cpp b/src/unittest/libboardgame_base/RatingTest.cpp new file mode 100644 index 0000000..2ec5031 --- /dev/null +++ b/src/unittest/libboardgame_base/RatingTest.cpp @@ -0,0 +1,70 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_base/RatingTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_base/Rating.h" +#include "libboardgame_test/Test.h" + +using namespace libboardgame_base; + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(boardgame_rating_get_expected_result) +{ + Rating a(2806); + Rating b(2577); + LIBBOARDGAME_CHECK_CLOSE_EPS(a.get_expected_result(b), 0.789, 0.001); +} + +LIBBOARDGAME_TEST_CASE(boardgame_rating_get_expected_result_multiplayer) +{ + // Player and 3 opponents, all with rating 1000, should have 25% + // winning probability + Rating a(1000); + Rating b(1000); + LIBBOARDGAME_CHECK_CLOSE_EPS(a.get_expected_result(b, 3), 0.25, 0.001); +} + +LIBBOARDGAME_TEST_CASE(boardgame_rating_update_1) +{ + Rating a(2806); + Rating b(2577); + Rating new_a = a; + Rating new_b = b; + new_a.update(0, b, 10); + new_b.update(1, a, 10); + LIBBOARDGAME_CHECK_CLOSE_EPS(new_a.get(), 2798.f, 1); + LIBBOARDGAME_CHECK_CLOSE_EPS(new_b.get(), 2585.f, 1); +} + +LIBBOARDGAME_TEST_CASE(boardgame_rating_update_2) +{ + Rating a(2806); + Rating b(2577); + Rating new_a = a; + Rating new_b = b; + new_a.update(1, b, 10); + new_b.update(0, a, 10); + LIBBOARDGAME_CHECK_CLOSE_EPS(new_a.get(), 2808.f, 1); + LIBBOARDGAME_CHECK_CLOSE_EPS(new_b.get(), 2575.f, 1); +} + +LIBBOARDGAME_TEST_CASE(boardgame_rating_update_3) +{ + Rating a(2806); + Rating b(2577); + Rating new_a = a; + Rating new_b = b; + new_a.update(0.5, b, 10); + new_b.update(0.5, a, 10); + LIBBOARDGAME_CHECK_CLOSE_EPS(new_a.get(), 2803.f, 1); + LIBBOARDGAME_CHECK_CLOSE_EPS(new_b.get(), 2580.f, 1); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_base/RectGeometryTest.cpp b/src/unittest/libboardgame_base/RectGeometryTest.cpp new file mode 100644 index 0000000..56a4a73 --- /dev/null +++ b/src/unittest/libboardgame_base/RectGeometryTest.cpp @@ -0,0 +1,90 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_base/RectGeometryTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_base/Point.h" +#include "libboardgame_base/RectGeometry.h" +#include "libboardgame_test/Test.h" + +using namespace std; + +//----------------------------------------------------------------------------- + +typedef libboardgame_base::Point<19 * 19, 19, 19, unsigned short> Point; +typedef libboardgame_base::Geometry Geometry; +typedef libboardgame_base::RectGeometry RectGeometry; +typedef libboardgame_base::ArrayList PointList; + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(boardgame_rect_geometry_iterate) +{ + auto& geo = RectGeometry::get(3, 3); + auto i = geo.begin(); + auto end = geo.end(); + LIBBOARDGAME_CHECK(i != end); + LIBBOARDGAME_CHECK(geo.get_point(0, 0) == *i); + ++i; + LIBBOARDGAME_CHECK(i != end); + LIBBOARDGAME_CHECK(geo.get_point(1, 0) == *i); + ++i; + LIBBOARDGAME_CHECK(i != end); + LIBBOARDGAME_CHECK(geo.get_point(2, 0) == *i); + ++i; + LIBBOARDGAME_CHECK(i != end); + LIBBOARDGAME_CHECK(geo.get_point(0, 1) == *i); + ++i; + LIBBOARDGAME_CHECK(i != end); + LIBBOARDGAME_CHECK(geo.get_point(1, 1) == *i); + ++i; + LIBBOARDGAME_CHECK(i != end); + LIBBOARDGAME_CHECK(geo.get_point(2, 1) == *i); + ++i; + LIBBOARDGAME_CHECK(i != end); + LIBBOARDGAME_CHECK(geo.get_point(0, 2) == *i); + ++i; + LIBBOARDGAME_CHECK(i != end); + LIBBOARDGAME_CHECK(geo.get_point(1, 2) == *i); + ++i; + LIBBOARDGAME_CHECK(i != end); + LIBBOARDGAME_CHECK(geo.get_point(2, 2) == *i); + ++i; + LIBBOARDGAME_CHECK(i == end); +} + +LIBBOARDGAME_TEST_CASE(boardgame_rect_geometry_from_string) +{ + auto& geo = RectGeometry::get(19, 19); + Point p; + + LIBBOARDGAME_CHECK(geo.from_string("a1", p)); + LIBBOARDGAME_CHECK(p == geo.get_point(0, 18)); + + LIBBOARDGAME_CHECK(geo.from_string("a19", p)); + LIBBOARDGAME_CHECK(p == geo.get_point(0, 0)); + + LIBBOARDGAME_CHECK(geo.from_string("A1", p)); + LIBBOARDGAME_CHECK(p == geo.get_point(0, 18)); + + LIBBOARDGAME_CHECK(! geo.from_string("foobar", p)); + LIBBOARDGAME_CHECK(! geo.from_string("a123", p)); + LIBBOARDGAME_CHECK(! geo.from_string("a56", p)); + LIBBOARDGAME_CHECK(! geo.from_string("aa1", p)); + LIBBOARDGAME_CHECK(! geo.from_string("c3#", p)); +} + +LIBBOARDGAME_TEST_CASE(boardgame_rect_geometry_to_string) +{ + auto& geo = RectGeometry::get(19, 19); + LIBBOARDGAME_CHECK_EQUAL(string("a1"), geo.to_string(geo.get_point(0, 18))); + LIBBOARDGAME_CHECK_EQUAL(string("a19"), geo.to_string(geo.get_point(0, 0))); + LIBBOARDGAME_CHECK_EQUAL(string("j10"), geo.to_string(geo.get_point(9, 9))); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_base/StringRepTest.cpp b/src/unittest/libboardgame_base/StringRepTest.cpp new file mode 100644 index 0000000..e348653 --- /dev/null +++ b/src/unittest/libboardgame_base/StringRepTest.cpp @@ -0,0 +1,86 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_base/StringRepTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include "libboardgame_base/StringRep.h" +#include "libboardgame_test/Test.h" + +using namespace std; +using libboardgame_base::StdStringRep; + +//----------------------------------------------------------------------------- + +namespace { + +StdStringRep string_rep; + +bool read(const string& s, unsigned& x, unsigned& y, unsigned width, + unsigned height) +{ + istringstream in(s); + return string_rep.read(in, width, height, x, y); +} + +string write(unsigned x, unsigned y, unsigned width, unsigned height) +{ + ostringstream out; + string_rep.write(out, x, y, width, height); + return out.str(); +} + +} // namespace + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(boardgame_base_spreadsheet_string_rep_read) +{ + unsigned x; + unsigned y; + + LIBBOARDGAME_CHECK(read("a1", x, y, 20, 20)); + LIBBOARDGAME_CHECK_EQUAL(x, 0u); + LIBBOARDGAME_CHECK_EQUAL(y, 19u); + + LIBBOARDGAME_CHECK(read("a23", x, y, 25, 25)); + LIBBOARDGAME_CHECK_EQUAL(x, 0u); + LIBBOARDGAME_CHECK_EQUAL(y, 2u); + + LIBBOARDGAME_CHECK(read("A1", x, y, 20, 20)); + LIBBOARDGAME_CHECK_EQUAL(x, 0u); + LIBBOARDGAME_CHECK_EQUAL(y, 19u); + + LIBBOARDGAME_CHECK(read("j1", x, y, 20, 20)); + LIBBOARDGAME_CHECK_EQUAL(x, 9u); + LIBBOARDGAME_CHECK_EQUAL(y, 19u); + + LIBBOARDGAME_CHECK(read("ab1", x, y, 30, 30)); + LIBBOARDGAME_CHECK_EQUAL(x, 27u); + LIBBOARDGAME_CHECK_EQUAL(y, 29u); + + LIBBOARDGAME_CHECK(read(" a1", x, y, 20, 20)); + LIBBOARDGAME_CHECK_EQUAL(x, 0u); + LIBBOARDGAME_CHECK_EQUAL(y, 19u); + + LIBBOARDGAME_CHECK(! read("a 1", x, y, 20, 20)); + + LIBBOARDGAME_CHECK(! read("foobar", x, y, 20, 20)); + + LIBBOARDGAME_CHECK(! read("c3#", x, y, 20, 20)); +} + +LIBBOARDGAME_TEST_CASE(boardgame_base_spreadsheet_string_rep_write) +{ + LIBBOARDGAME_CHECK_EQUAL(string("a1"), write(0, 18, 19, 19)); + LIBBOARDGAME_CHECK_EQUAL(string("a19"), write(0, 0, 19, 19)); + LIBBOARDGAME_CHECK_EQUAL(string("ab1"), write(27, 59, 60, 60)); + LIBBOARDGAME_CHECK_EQUAL(string("ba1"), write(52, 59, 60, 60)); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_gtp/ArgumentsTest.cpp b/src/unittest/libboardgame_gtp/ArgumentsTest.cpp new file mode 100644 index 0000000..a42cae9 --- /dev/null +++ b/src/unittest/libboardgame_gtp/ArgumentsTest.cpp @@ -0,0 +1,157 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_gtp/ArgumentsTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_gtp/Arguments.h" +#include "libboardgame_test/Test.h" + +using namespace std; +using namespace libboardgame_gtp; + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(gtp_arguments_arg) +{ + CmdLine line("command arg1 \"arg2 \" arg3 "); + Arguments args(line); + LIBBOARDGAME_CHECK_EQUAL("arg1", string(args.get(0))); + LIBBOARDGAME_CHECK_EQUAL("arg2 ", string(args.get(1))); + LIBBOARDGAME_CHECK_EQUAL("arg3", string(args.get(2))); +} + +LIBBOARDGAME_TEST_CASE(gtp_arguments_to_lower) +{ + CmdLine line("command cAsE"); + Arguments args(line); + LIBBOARDGAME_CHECK_EQUAL(string("case"), args.get_tolower(0)); +} + +LIBBOARDGAME_TEST_CASE(gtp_arguments_bool) +{ + { + CmdLine line("command 0"); + Arguments args(line); + LIBBOARDGAME_CHECK(! args.parse(0)); + } + { + CmdLine line("command 1"); + Arguments args(line); + LIBBOARDGAME_CHECK(args.parse(0)); + } + { + CmdLine line("command 2"); + Arguments args(line); + LIBBOARDGAME_CHECK_THROW(args.parse(0), Failure); + } + { + CmdLine line("command arg1"); + Arguments args(line); + LIBBOARDGAME_CHECK_THROW(args.parse(0), Failure); + } + { + CmdLine line("command"); + Arguments args(line); + LIBBOARDGAME_CHECK_THROW(args.parse(0), Failure); + } +} + +LIBBOARDGAME_TEST_CASE(gtp_arguments_float) +{ + CmdLine line("command abc 5.5"); + Arguments args(line); + LIBBOARDGAME_CHECK_THROW(args.parse(0), Failure); + LIBBOARDGAME_CHECK_CLOSE(5.5f, args.parse(1), 1e-4); +} + +LIBBOARDGAME_TEST_CASE(gtp_arguments_int) +{ + CmdLine line("command 5 arg"); + Arguments args(line); + LIBBOARDGAME_CHECK_EQUAL(5, args.parse(0)); + LIBBOARDGAME_CHECK_THROW(args.parse(1), Failure); +} + +LIBBOARDGAME_TEST_CASE(gtp_arguments_min_int) +{ + CmdLine line("command 5"); + Arguments args(line); + LIBBOARDGAME_CHECK_EQUAL(5, args.parse_min(0, 3)); + LIBBOARDGAME_CHECK_THROW(args.parse_min(0, 7), Failure); +} + +LIBBOARDGAME_TEST_CASE(gtp_arguments_min_max_int) +{ + CmdLine line("command 5"); + Arguments args(line); + LIBBOARDGAME_CHECK_EQUAL(5, args.parse_min_max(0, 3, 10)); + LIBBOARDGAME_CHECK_THROW(args.parse_min_max(0, 0, 4), Failure); + LIBBOARDGAME_CHECK_THROW(args.parse_min_max(0, 10, 20), Failure); +} + +LIBBOARDGAME_TEST_CASE(gtp_arguments_single_int) +{ + { + CmdLine line("command 5"); + Arguments args(line); + LIBBOARDGAME_CHECK_EQUAL(5, args.parse()); + } + { + CmdLine line("command 5 10"); + Arguments args(line); + LIBBOARDGAME_CHECK_THROW(args.parse(), Failure); + } +} + +LIBBOARDGAME_TEST_CASE(gtp_arguments_nu_arg_0) +{ + CmdLine line("1 command"); + Arguments args(line); + LIBBOARDGAME_CHECK_NO_THROW(args.check_empty()); + LIBBOARDGAME_CHECK_THROW(args.check_size(1), Failure); + LIBBOARDGAME_CHECK_NO_THROW(args.check_size_less_equal(2)); +} + +LIBBOARDGAME_TEST_CASE(gtp_arguments_nu_arg_3) +{ + CmdLine line("command arg1 arg2 arg3"); + Arguments args(line); + LIBBOARDGAME_CHECK_THROW(args.check_empty(), Failure); + LIBBOARDGAME_CHECK_THROW(args.check_size(2), Failure); + LIBBOARDGAME_CHECK_NO_THROW(args.check_size(3)); + LIBBOARDGAME_CHECK_THROW(args.check_size(4), Failure); + LIBBOARDGAME_CHECK_THROW(args.check_size_less_equal(2), Failure); + LIBBOARDGAME_CHECK_NO_THROW(args.check_size_less_equal(3)); + LIBBOARDGAME_CHECK_NO_THROW(args.check_size_less_equal(4)); +} + +LIBBOARDGAME_TEST_CASE(gtp_arguments_remaining_arg) +{ + CmdLine line("command arg1 arg2"); + Arguments args(line); + LIBBOARDGAME_CHECK_EQUAL("arg2", string(args.get_remaining_line(0))); +} + +LIBBOARDGAME_TEST_CASE(gtp_arguments_remaining_arg_empty) +{ + CmdLine line("command arg1"); + Arguments args(line); + LIBBOARDGAME_CHECK_EQUAL("", string(args.get_remaining_line(0))); +} + +LIBBOARDGAME_TEST_CASE(gtp_arguments_remaining_line) +{ + CmdLine line("command arg1 \"arg2 \" arg3 "); + Arguments args(line); + LIBBOARDGAME_CHECK_EQUAL("\"arg2 \" arg3", + string(args.get_remaining_line(0))); + LIBBOARDGAME_CHECK_EQUAL("arg3", string(args.get_remaining_line(1))); + LIBBOARDGAME_CHECK_EQUAL("", string(args.get_remaining_line(2))); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_gtp/CMakeLists.txt b/src/unittest/libboardgame_gtp/CMakeLists.txt new file mode 100644 index 0000000..ecd1ca1 --- /dev/null +++ b/src/unittest/libboardgame_gtp/CMakeLists.txt @@ -0,0 +1,15 @@ +add_executable(unittest_libboardgame_gtp + ArgumentsTest.cpp + CmdLineTest.cpp + EngineTest.cpp + ResponseTest.cpp +) + +target_link_libraries(unittest_libboardgame_gtp + boardgame_test_main + boardgame_test + boardgame_util + boardgame_gtp + ) + +add_test(libboardgame_gtp unittest_libboardgame_gtp) diff --git a/src/unittest/libboardgame_gtp/CmdLineTest.cpp b/src/unittest/libboardgame_gtp/CmdLineTest.cpp new file mode 100644 index 0000000..8ef988d --- /dev/null +++ b/src/unittest/libboardgame_gtp/CmdLineTest.cpp @@ -0,0 +1,70 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_gtp/CmdLineTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_gtp/CmdLine.h" +#include "libboardgame_test/Test.h" + +using namespace std; +using namespace libboardgame_gtp; + +//----------------------------------------------------------------------------- + +namespace { + +string get_id(const CmdLine& c) +{ + ostringstream s; + c.write_id(s); + return s.str(); +} + +string get_element(const CmdLine& c, unsigned i) +{ + return string(c.get_element(i)); +} + +} + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(gtp_cmd_line_init) +{ + CmdLine c("100 command1 arg1 arg2"); + LIBBOARDGAME_CHECK_EQUAL("100", get_id(c)); + LIBBOARDGAME_CHECK_EQUAL("command1", string(c.get_name())); + LIBBOARDGAME_CHECK_EQUAL(4u, c.get_elements().size()); + LIBBOARDGAME_CHECK_EQUAL("arg1", get_element(c, 2)); + LIBBOARDGAME_CHECK_EQUAL("arg2", get_element(c, 3)); + c.init("2 command2 arg3"); + LIBBOARDGAME_CHECK_EQUAL("2", get_id(c)); + LIBBOARDGAME_CHECK_EQUAL("command2", string(c.get_name())); + LIBBOARDGAME_CHECK_EQUAL(3u, c.get_elements().size()); + LIBBOARDGAME_CHECK_EQUAL("arg3", get_element(c, 2)); +} + +LIBBOARDGAME_TEST_CASE(gtp_cmd_line_parse) +{ + CmdLine c("10 boardsize 11"); + LIBBOARDGAME_CHECK_EQUAL("10 boardsize 11", c.get_line()); + LIBBOARDGAME_CHECK_EQUAL("11", string(c.get_trimmed_line_after_elem(1))); + LIBBOARDGAME_CHECK_EQUAL("10", get_id(c)); + LIBBOARDGAME_CHECK_EQUAL("boardsize", string(c.get_name())); + LIBBOARDGAME_CHECK_EQUAL(3u, c.get_elements().size()); + LIBBOARDGAME_CHECK_EQUAL("11", get_element(c, 2)); + + c.init(" 20 clear_board "); + LIBBOARDGAME_CHECK_EQUAL(" 20 clear_board ", c.get_line()); + LIBBOARDGAME_CHECK_EQUAL("", string(c.get_trimmed_line_after_elem(1))); + LIBBOARDGAME_CHECK_EQUAL("20", get_id(c)); + LIBBOARDGAME_CHECK_EQUAL("clear_board", string(c.get_name())); + LIBBOARDGAME_CHECK_EQUAL(2u, c.get_elements().size()); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_gtp/EngineTest.cpp b/src/unittest/libboardgame_gtp/EngineTest.cpp new file mode 100644 index 0000000..a501e0c --- /dev/null +++ b/src/unittest/libboardgame_gtp/EngineTest.cpp @@ -0,0 +1,121 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_gtp/EngineTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_gtp/Engine.h" +#include "libboardgame_test/Test.h" + +using namespace std; +using namespace libboardgame_gtp; + +//----------------------------------------------------------------------------- + +namespace { + +//----------------------------------------------------------------------------- + +/** GTP engine returning invalid responses for testing class Engine. + For testing that the base class Engine sanitizes responses of + subclasses that contain empty lines (see @ref Engine::exec_main_loop). */ +class InvalidResponseEngine + : public Engine +{ +public: + InvalidResponseEngine(); + + void invalid_response(const Arguments&, Response&); + + void invalid_response_2(const Arguments&, Response&); +}; + +InvalidResponseEngine::InvalidResponseEngine() +{ + add("invalid_response", &InvalidResponseEngine::invalid_response); + add("invalid_response_2", &InvalidResponseEngine::invalid_response_2); +} + +void InvalidResponseEngine::invalid_response(const Arguments&, Response& r) +{ + r << "This response is invalid\n" + << "\n" + << "because it contains an empty line"; +} + +void InvalidResponseEngine::invalid_response_2(const Arguments&, Response& r) +{ + r << "This response is invalid\n" + << "\n" + << "\n" + << "because it contains two empty lines"; +} + +//----------------------------------------------------------------------------- + +} // namespace + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(gtp_engine_command) +{ + istringstream in("version\n"); + ostringstream out; + Engine engine; + engine.exec_main_loop(in, out); + LIBBOARDGAME_CHECK_EQUAL(string("= \n\n"), out.str()); +} + +LIBBOARDGAME_TEST_CASE(gtp_engine_command_with_id) +{ + istringstream in("10 version\n"); + ostringstream out; + Engine engine; + engine.exec_main_loop(in, out); + LIBBOARDGAME_CHECK_EQUAL(string("=10 \n\n"), out.str()); +} + +/** Check that invalid responses with one empty line are sanitized. */ +LIBBOARDGAME_TEST_CASE(gtp_engine_empty_lines) +{ + istringstream in("invalid_response\n"); + ostringstream out; + InvalidResponseEngine engine; + engine.exec_main_loop(in, out); + LIBBOARDGAME_CHECK_EQUAL(string("= This response is invalid\n" + " \n" + "because it contains an empty line\n" + "\n"), + out.str()); +} + +/** Check that invalid responses with two empty lines are sanitized. */ +LIBBOARDGAME_TEST_CASE(gtp_engine_empty_lines_2) +{ + istringstream in("invalid_response_2\n"); + ostringstream out; + InvalidResponseEngine engine; + engine.exec_main_loop(in, out); + LIBBOARDGAME_CHECK_EQUAL(string("= This response is invalid\n" + " \n" + " \n" + "because it contains two empty lines\n" + "\n"), + out.str()); +} + +LIBBOARDGAME_TEST_CASE(gtp_engine_unknown_command) +{ + istringstream in("unknowncommand\n"); + ostringstream out; + Engine engine; + engine.exec_main_loop(in, out); + LIBBOARDGAME_CHECK(out.str().size() >= 2); + LIBBOARDGAME_CHECK_EQUAL(string("? "), out.str().substr(0, 2)); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_gtp/ResponseTest.cpp b/src/unittest/libboardgame_gtp/ResponseTest.cpp new file mode 100644 index 0000000..d025021 --- /dev/null +++ b/src/unittest/libboardgame_gtp/ResponseTest.cpp @@ -0,0 +1,28 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_gtp/ResponseTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_gtp/Response.h" +#include "libboardgame_test/Test.h" + +using namespace std; +using namespace libboardgame_gtp; + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(gtp_response_basic) +{ + Response r; + r << "Name"; + LIBBOARDGAME_CHECK_EQUAL(string("Name"), r.to_string()); + r.set("Name2"); + LIBBOARDGAME_CHECK_EQUAL(string("Name2"), r.to_string()); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_mcts/CMakeLists.txt b/src/unittest/libboardgame_mcts/CMakeLists.txt new file mode 100644 index 0000000..83ca0a6 --- /dev/null +++ b/src/unittest/libboardgame_mcts/CMakeLists.txt @@ -0,0 +1,13 @@ +add_executable(unittest_libboardgame_mcts + NodeTest.cpp +) + +target_link_libraries(unittest_libboardgame_mcts + boardgame_test_main + boardgame_test + boardgame_sgf + boardgame_util + boardgame_sys + ) + +add_test(libboardgame_mcts unittest_libboardgame_mcts) diff --git a/src/unittest/libboardgame_mcts/NodeTest.cpp b/src/unittest/libboardgame_mcts/NodeTest.cpp new file mode 100644 index 0000000..e09bffe --- /dev/null +++ b/src/unittest/libboardgame_mcts/NodeTest.cpp @@ -0,0 +1,41 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_mcts/NodeTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_mcts/Node.h" + +#include "libboardgame_test/Test.h" + +using namespace std; + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(libboardgame_mcts_node_add_value) +{ + libboardgame_mcts::Node node; + node.init(0, 0.5, 0); + node.add_value(5); + LIBBOARDGAME_CHECK_CLOSE(node.get_value(), 5., 1e-4); + node.add_value(2); + LIBBOARDGAME_CHECK_CLOSE(node.get_value(), 3.5, 1e-4); +} + +LIBBOARDGAME_TEST_CASE(libboardgame_mcts_node_add_value_remove_loss) +{ + libboardgame_mcts::Node node; + node.init(0, 0.5, 0); + node.add_value(5); + LIBBOARDGAME_CHECK_CLOSE(node.get_value(), 5., 1e-4); + node.add_value(0); + LIBBOARDGAME_CHECK_CLOSE(node.get_value(), 2.5, 1e-4); + node.add_value_remove_loss(2); + LIBBOARDGAME_CHECK_CLOSE(node.get_value(), 3.5, 1e-4); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_sgf/CMakeLists.txt b/src/unittest/libboardgame_sgf/CMakeLists.txt new file mode 100644 index 0000000..b50e7b8 --- /dev/null +++ b/src/unittest/libboardgame_sgf/CMakeLists.txt @@ -0,0 +1,14 @@ +add_executable(unittest_libboardgame_sgf + SgfNodeTest.cpp + SgfUtilTest.cpp + TreeReaderTest.cpp +) + +target_link_libraries(unittest_libboardgame_sgf + boardgame_test_main + boardgame_test + boardgame_sgf + boardgame_util + ) + +add_test(libboardgame_sgf unittest_libboardgame_sgf) diff --git a/src/unittest/libboardgame_sgf/SgfNodeTest.cpp b/src/unittest/libboardgame_sgf/SgfNodeTest.cpp new file mode 100644 index 0000000..7acd4ca --- /dev/null +++ b/src/unittest/libboardgame_sgf/SgfNodeTest.cpp @@ -0,0 +1,41 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_sgf/SgfNodeTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include "libboardgame_sgf/SgfNode.h" +#include "libboardgame_test/Test.h" + +using namespace std; +using namespace libboardgame_sgf; + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(sgf_node_create_new_child) +{ + unique_ptr parent(new SgfNode); + auto& child = parent->create_new_child(); + LIBBOARDGAME_CHECK_EQUAL(&parent->get_child(), &child); + LIBBOARDGAME_CHECK_EQUAL(&child.get_parent(), parent.get()); +} + +LIBBOARDGAME_TEST_CASE(sgf_node_remove_property) +{ + string id = "B"; + unique_ptr node(new SgfNode); + LIBBOARDGAME_CHECK(! node->has_property(id)); + node->set_property(id, "foo"); + LIBBOARDGAME_CHECK(node->has_property(id)); + LIBBOARDGAME_CHECK_EQUAL(node->get_property(id), "foo"); + bool result = node->remove_property(id); + LIBBOARDGAME_CHECK(result); + LIBBOARDGAME_CHECK(! node->has_property(id)); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_sgf/SgfUtilTest.cpp b/src/unittest/libboardgame_sgf/SgfUtilTest.cpp new file mode 100644 index 0000000..6421fb8 --- /dev/null +++ b/src/unittest/libboardgame_sgf/SgfUtilTest.cpp @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_sgf/SgfUtilTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_sgf/SgfUtil.h" + +#include "libboardgame_test/Test.h" + +using namespace std; +using namespace libboardgame_sgf; +using namespace libboardgame_sgf::util; + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(sgf_util_get_path_from_root) +{ + unique_ptr root(new SgfNode); + auto& child = root->create_new_child(); + vector path; + get_path_from_root(child, path); + LIBBOARDGAME_CHECK_EQUAL(path.size(), 2u); + LIBBOARDGAME_CHECK_EQUAL(path[0], root.get()); + LIBBOARDGAME_CHECK_EQUAL(path[1], &child); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_sgf/TreeReaderTest.cpp b/src/unittest/libboardgame_sgf/TreeReaderTest.cpp new file mode 100644 index 0000000..6db65c1 --- /dev/null +++ b/src/unittest/libboardgame_sgf/TreeReaderTest.cpp @@ -0,0 +1,122 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_sgf/TreeReaderTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_sgf/TreeReader.h" + +#include +#include "libboardgame_sgf/TreeWriter.h" +#include "libboardgame_test/Test.h" + +using namespace std; +using namespace libboardgame_sgf; + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(sgf_tree_reader_basic) +{ + istringstream in("(;B[aa];W[bb])"); + TreeReader reader; + reader.read(in); + auto& root = reader.get_tree(); + LIBBOARDGAME_CHECK(root.has_property("B")); + LIBBOARDGAME_CHECK(root.has_single_child()); + auto& child = root.get_child(); + LIBBOARDGAME_CHECK(child.has_property("W")); + LIBBOARDGAME_CHECK(! child.has_children()); +} + +LIBBOARDGAME_TEST_CASE(sgf_tree_reader_basic_2) +{ + istringstream in("(;C[1](;C[2.1])(;C[2.2]))"); + TreeReader reader; + reader.read(in); + auto& root = reader.get_tree(); + LIBBOARDGAME_CHECK_EQUAL(root.get_property("C"), "1"); + LIBBOARDGAME_CHECK_EQUAL(root.get_nu_children(), 2u); + LIBBOARDGAME_CHECK_EQUAL(root.get_child(0).get_property("C"), "2.1"); + LIBBOARDGAME_CHECK_EQUAL(root.get_child(1).get_property("C"), "2.2"); +} + +/** Test that a property value with a unicode character is preserved after + reading and writing. + In previous versions this was broken because of a bug in the replacement + of non-newline whitespaces (as required by SGF) by the writer. (The bug + occurred only on some platforms depending on the std::isspace() + implementation.) */ +LIBBOARDGAME_TEST_CASE(sgf_tree_reader_unicode) +{ + SgfNode root; + const char* id = "C"; + const char* value = "\xc3\xbc"; // German u-umlaut as UTF-8 + root.set_property(id, value); + ostringstream out; + TreeWriter writer(out, root); + writer.write(); + istringstream in(out.str()); + TreeReader reader; + reader.read(in); + LIBBOARDGAME_CHECK_EQUAL(reader.get_tree().get_property(id), value); +} + +LIBBOARDGAME_TEST_CASE(sgf_tree_reader_property_after_newline) +{ + istringstream in("(;FF[4]\n" + "CA[UTF-8])"); + TreeReader reader; + reader.read(in); + auto& root = reader.get_tree(); + LIBBOARDGAME_CHECK(root.has_property("FF")); + LIBBOARDGAME_CHECK(root.has_property("CA")); +} + +/** Test cross-platform handling of property values containing newlines. + The reader should convert all platform-dependent newline sequences (LF, + CR+LF, CR) into LF, such that property values containing newlines are + independent on the platform that was used to write the file. */ +LIBBOARDGAME_TEST_CASE(sgf_tree_reader_newline) +{ + { + istringstream in("(;C[1\n2])"); + TreeReader reader; + reader.read(in); + auto& root = reader.get_tree(); + LIBBOARDGAME_CHECK_EQUAL(root.get_property("C"), "1\n2"); + } + { + istringstream in("(;C[1\r\n2])"); + TreeReader reader; + reader.read(in); + auto& root = reader.get_tree(); + LIBBOARDGAME_CHECK_EQUAL(root.get_property("C"), "1\n2"); + } + { + istringstream in("(;C[1\r2])"); + TreeReader reader; + reader.read(in); + auto& root = reader.get_tree(); + LIBBOARDGAME_CHECK_EQUAL(root.get_property("C"), "1\n2"); + } +} + +LIBBOARDGAME_TEST_CASE(sgf_tree_reader_property_without_value) +{ + istringstream in("(;B)"); + TreeReader reader; + LIBBOARDGAME_CHECK_THROW(reader.read(in), TreeReader::ReadError); +} + +LIBBOARDGAME_TEST_CASE(sgf_tree_reader_text_before_node) +{ + istringstream in("(B;)"); + TreeReader reader; + LIBBOARDGAME_CHECK_THROW(reader.read(in), TreeReader::ReadError); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_util/ArrayListTest.cpp b/src/unittest/libboardgame_util/ArrayListTest.cpp new file mode 100644 index 0000000..05f00c1 --- /dev/null +++ b/src/unittest/libboardgame_util/ArrayListTest.cpp @@ -0,0 +1,74 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_util/ArrayListTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_util/ArrayList.h" +#include "libboardgame_test/Test.h" + +using namespace std; +using namespace libboardgame_util; + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(util_array_list_basic) +{ + ArrayList l; + LIBBOARDGAME_CHECK_EQUAL(0u, l.size()); + LIBBOARDGAME_CHECK(l.empty()); + l.push_back(5); + LIBBOARDGAME_CHECK_EQUAL(1u, l.size()); + LIBBOARDGAME_CHECK(! l.empty()); + LIBBOARDGAME_CHECK_EQUAL(5, l[0]); + l.push_back(7); + LIBBOARDGAME_CHECK_EQUAL(2u, l.size()); + LIBBOARDGAME_CHECK(! l.empty()); + LIBBOARDGAME_CHECK_EQUAL(5, l[0]); + LIBBOARDGAME_CHECK_EQUAL(7, l[1]); + l.clear(); + LIBBOARDGAME_CHECK_EQUAL(0u, l.size()); + LIBBOARDGAME_CHECK(l.empty()); +} + +LIBBOARDGAME_TEST_CASE(util_array_list_construct_single_element) +{ + ArrayList l(5); + LIBBOARDGAME_CHECK_EQUAL(1u, l.size()); + LIBBOARDGAME_CHECK_EQUAL(5, l[0]); +} + +LIBBOARDGAME_TEST_CASE(util_array_list_equals) +{ + ArrayList l1{ 1, 2, 3 }; + ArrayList l2{ 1, 2, 3 }; + LIBBOARDGAME_CHECK(l1 == l2); + l2.push_back(4); + LIBBOARDGAME_CHECK(! (l1 == l2)); + l2 = ArrayList({ 2, 1, 3 }); + LIBBOARDGAME_CHECK(! (l1 == l2)); +} + +LIBBOARDGAME_TEST_CASE(util_array_list_pop_back) +{ + ArrayList l(5); + int i = l.pop_back(); + LIBBOARDGAME_CHECK_EQUAL(5, i); + LIBBOARDGAME_CHECK(l.empty()); +} + +LIBBOARDGAME_TEST_CASE(util_array_list_remove) +{ + ArrayList l{ 1, 2, 3, 4 }; + l.remove(2); + LIBBOARDGAME_CHECK_EQUAL(3u, l.size()); + LIBBOARDGAME_CHECK_EQUAL(1, l[0]); + LIBBOARDGAME_CHECK_EQUAL(3, l[1]); + LIBBOARDGAME_CHECK_EQUAL(4, l[2]); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_util/CMakeLists.txt b/src/unittest/libboardgame_util/CMakeLists.txt new file mode 100644 index 0000000..c157257 --- /dev/null +++ b/src/unittest/libboardgame_util/CMakeLists.txt @@ -0,0 +1,15 @@ +add_executable(unittest_libboardgame_util + ArrayListTest.cpp + OptionsTest.cpp + StatisticsTest.cpp + StringUtilTest.cpp +) + +target_link_libraries(unittest_libboardgame_util + boardgame_test_main + boardgame_test + boardgame_util + boardgame_sys + ) + +add_test(libboardgame_util unittest_libboardgame_util) diff --git a/src/unittest/libboardgame_util/OptionsTest.cpp b/src/unittest/libboardgame_util/OptionsTest.cpp new file mode 100644 index 0000000..842e9ea --- /dev/null +++ b/src/unittest/libboardgame_util/OptionsTest.cpp @@ -0,0 +1,91 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_util/OptionsTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_util/Options.h" +#include "libboardgame_test/Test.h" + +using namespace std; +using namespace libboardgame_util; + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(libboardgame_util_options_basic) +{ + vector specs = + { "first|a:", "second|b:", "third|c", "fourth", "fifth" }; + const char* argv[] = + { nullptr, "--second", "secondval", "--first", "firstval", + "--fourth", "-c", "arg1", "arg2" }; + int argc = static_cast(sizeof(argv) / sizeof(argv[0])); + Options opt(argc, argv, specs); + LIBBOARDGAME_CHECK(opt.contains("first")); + LIBBOARDGAME_CHECK_EQUAL(opt.get("first"), "firstval"); + LIBBOARDGAME_CHECK(opt.contains("second")); + LIBBOARDGAME_CHECK_EQUAL(opt.get("second"), "secondval"); + LIBBOARDGAME_CHECK(opt.contains("third")); + LIBBOARDGAME_CHECK(opt.contains("fourth")); + LIBBOARDGAME_CHECK(! opt.contains("fifth")); + auto& args = opt.get_args(); + LIBBOARDGAME_CHECK_EQUAL(args.size(), 2u); + LIBBOARDGAME_CHECK_EQUAL(args[0], "arg1"); + LIBBOARDGAME_CHECK_EQUAL(args[1], "arg2"); +} + +LIBBOARDGAME_TEST_CASE(libboardgame_util_options_end_options) +{ + vector specs = { "first:" }; + const char* argv[] = + { nullptr, "--first", "firstval", "--", "--arg1" }; + int argc = static_cast(sizeof(argv) / sizeof(argv[0])); + Options opt(argc, argv, specs); + LIBBOARDGAME_CHECK_EQUAL(opt.get("first"), "firstval"); + auto& args = opt.get_args(); + LIBBOARDGAME_CHECK_EQUAL(args.size(), 1u); + LIBBOARDGAME_CHECK_EQUAL(args[0], "--arg1"); +} + +LIBBOARDGAME_TEST_CASE(libboardgame_util_options_missing_val) +{ + vector specs = { "first:" }; + const char* argv[] = { nullptr, "--first" }; + int argc = static_cast(sizeof(argv) / sizeof(argv[0])); + LIBBOARDGAME_CHECK_THROW(Options opt(argc, argv, specs), runtime_error); +} + +LIBBOARDGAME_TEST_CASE(libboardgame_util_options_nospace) +{ + vector specs = { "first|a:", "second|b:" }; + const char* argv[] = { nullptr, "-abc" }; + int argc = static_cast(sizeof(argv) / sizeof(argv[0])); + Options opt(argc, argv, specs); + LIBBOARDGAME_CHECK_EQUAL(opt.get("first"), "bc"); +} + +LIBBOARDGAME_TEST_CASE(libboardgame_util_options_multi_short_with_val) +{ + vector specs = { "first|a", "second|b:" }; + const char* argv[] = { nullptr, "-ab", "c" }; + int argc = static_cast(sizeof(argv) / sizeof(argv[0])); + Options opt(argc, argv, specs); + LIBBOARDGAME_CHECK(opt.contains("first")); + LIBBOARDGAME_CHECK_EQUAL(opt.get("second"), "c"); +} + +LIBBOARDGAME_TEST_CASE(libboardgame_util_options_type) +{ + vector specs = { "first:", "second:" }; + const char* argv[] = { nullptr, "--first", "10", "--second", "foo" }; + int argc = static_cast(sizeof(argv) / sizeof(argv[0])); + Options opt(argc, argv, specs); + LIBBOARDGAME_CHECK_EQUAL(opt.get("first"), 10); + LIBBOARDGAME_CHECK_THROW(opt.get("second"), runtime_error); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_util/StatisticsTest.cpp b/src/unittest/libboardgame_util/StatisticsTest.cpp new file mode 100644 index 0000000..0084a73 --- /dev/null +++ b/src/unittest/libboardgame_util/StatisticsTest.cpp @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_util/StatisticsTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_util/Statistics.h" +#include "libboardgame_test/Test.h" + +using namespace std; +using namespace libboardgame_util; + +//----------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(libboardgame_util_statistics_basic) +{ + Statistics s; + s.add(12); + s.add(11); + s.add(14); + s.add(16); + s.add(15); + LIBBOARDGAME_CHECK_EQUAL(s.get_count(), 5.); + LIBBOARDGAME_CHECK_CLOSE_EPS(s.get_mean(), 13.6, 1e-6); + LIBBOARDGAME_CHECK_CLOSE_EPS(s.get_variance(), 3.44, 1e-6); + LIBBOARDGAME_CHECK_CLOSE_EPS(s.get_deviation(), 1.854723, 1e-6); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_util/StringUtilTest.cpp b/src/unittest/libboardgame_util/StringUtilTest.cpp new file mode 100644 index 0000000..0f10f5f --- /dev/null +++ b/src/unittest/libboardgame_util/StringUtilTest.cpp @@ -0,0 +1,97 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_util/StringUtilTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_util/StringUtil.h" +#include "libboardgame_test/Test.h" + +using namespace std; +using namespace libboardgame_util; + +//---------------------------------------------------------------------------- + +LIBBOARDGAME_TEST_CASE(libboardgame_util_get_letter_coord) +{ + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(0), "a"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(1), "b"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(25), "z"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(26), "aa"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(26 + 1), "ab"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(26 + 25), "az"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(2 * 26), "ba"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(2 * 26 + 1), "bb"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(2 * 26 + 25), "bz"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(26 * 26), "za"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(26 * 26 + 1), "zb"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(26 * 26 + 25), "zz"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(27 * 26), "aaa"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(27 * 26 + 1), "aab"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(27 * 26 + 25), "aaz"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(28 * 26), "aba"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(28 * 26 + 1), "abb"); + LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(28 * 26 + 25), "abz"); +} + +LIBBOARDGAME_TEST_CASE(libboardgame_util_split) +{ + { + vector v = split("a,b,cc,d", ','); + LIBBOARDGAME_CHECK_EQUAL(v.size(), 4u); + LIBBOARDGAME_CHECK_EQUAL(v[0], "a"); + LIBBOARDGAME_CHECK_EQUAL(v[1], "b"); + LIBBOARDGAME_CHECK_EQUAL(v[2], "cc"); + LIBBOARDGAME_CHECK_EQUAL(v[3], "d"); + } + { + vector v = split("", ','); + LIBBOARDGAME_CHECK_EQUAL(v.size(), 0u); + } + { + vector v = split("a,", ','); + LIBBOARDGAME_CHECK_EQUAL(v.size(), 2u); + LIBBOARDGAME_CHECK_EQUAL(v[0], "a"); + LIBBOARDGAME_CHECK_EQUAL(v[1], ""); + } + { + vector v = split(",a", ','); + LIBBOARDGAME_CHECK_EQUAL(v.size(), 2u); + LIBBOARDGAME_CHECK_EQUAL(v[0], ""); + LIBBOARDGAME_CHECK_EQUAL(v[1], "a"); + } + { + vector v = split("a,,b", ','); + LIBBOARDGAME_CHECK_EQUAL(v.size(), 3u); + LIBBOARDGAME_CHECK_EQUAL(v[0], "a"); + LIBBOARDGAME_CHECK_EQUAL(v[1], ""); + LIBBOARDGAME_CHECK_EQUAL(v[2], "b"); + } +} + +LIBBOARDGAME_TEST_CASE(libboardgame_util_to_lower) +{ + LIBBOARDGAME_CHECK_EQUAL(to_lower("AabC "), "aabc "); +} + +LIBBOARDGAME_TEST_CASE(libboardgame_util_trim) +{ + LIBBOARDGAME_CHECK_EQUAL(trim("aa bb"), "aa bb"); + LIBBOARDGAME_CHECK_EQUAL(trim(" \t\r\naa bb"), "aa bb"); + LIBBOARDGAME_CHECK_EQUAL(trim("aa bb \t\r\n"), "aa bb"); + LIBBOARDGAME_CHECK_EQUAL(trim(""), ""); +} + +LIBBOARDGAME_TEST_CASE(libboardgame_util_trim_right) +{ + LIBBOARDGAME_CHECK_EQUAL(trim_right("aa bb"), "aa bb"); + LIBBOARDGAME_CHECK_EQUAL(trim_right(" \t\r\naa bb"), " \t\r\naa bb"); + LIBBOARDGAME_CHECK_EQUAL(trim_right("aa bb \t\r\n"), "aa bb"); + LIBBOARDGAME_CHECK_EQUAL(trim_right(""), ""); +} + +//---------------------------------------------------------------------------- diff --git a/src/unittest/libpentobi_base/BoardConstTest.cpp b/src/unittest/libpentobi_base/BoardConstTest.cpp new file mode 100644 index 0000000..e8ed58d --- /dev/null +++ b/src/unittest/libpentobi_base/BoardConstTest.cpp @@ -0,0 +1,42 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libpentobi_base/BoardConstTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libpentobi_base/BoardConst.h" + +#include "libboardgame_test/Test.h" + +using namespace std; +using namespace libpentobi_base; + +//----------------------------------------------------------------------------- + +/** Test that points in move strings are ordered. + As specified in doc/blksgf/Pentobi-SGF.html, the order should be + (a1, b1, ..., a2, b2, ...). There is no restriction on the order when + parsing move strings in from_string(). */ +LIBBOARDGAME_TEST_CASE(pentobi_base_board_const_move_string) +{ + auto& bc = BoardConst::get(Variant::duo); + Move mv = bc.from_string("h7,i7,i6,j6,j5"); + LIBBOARDGAME_CHECK_EQUAL(bc.to_string(mv), "j5,i6,j6,h7,i7"); +} + +/** Check symmetry information in MoveInfoExt for some moves. */ +LIBBOARDGAME_TEST_CASE(pentobi_base_board_const_symmetry_info) +{ + auto& bc = BoardConst::get(Variant::trigon_2); + auto& info_ext_2 = + bc.get_move_info_ext_2(bc.from_string("q9,q10,r10,q11,r11,s11")); + LIBBOARDGAME_CHECK(! info_ext_2.breaks_symmetry); + LIBBOARDGAME_CHECK_EQUAL(info_ext_2.symmetric_move.to_int(), + bc.from_string("q8,r8,s8,r9,s9,s10").to_int()); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libpentobi_base/BoardTest.cpp b/src/unittest/libpentobi_base/BoardTest.cpp new file mode 100644 index 0000000..a5b78b0 --- /dev/null +++ b/src/unittest/libpentobi_base/BoardTest.cpp @@ -0,0 +1,165 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libpentobi_base/BoardTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libpentobi_base/Board.h" + +#include "libboardgame_test/Test.h" +#include "libpentobi_base/MoveMarker.h" + +using namespace std; +using namespace libpentobi_base; + +//----------------------------------------------------------------------------- + +namespace { + +void play(Board& bd, Color c, const char* s) +{ + bd.play(c, bd.from_string(s)); +} + +} // namespace + +//----------------------------------------------------------------------------- + +/** Check some basic functions in a Classic Two-Player game. */ +LIBBOARDGAME_TEST_CASE(pentobi_base_board_classic_2) +{ + /* + ( + ;GM[Blokus Two-Player] + ;1[a20,b20,c20,d20,e20] + ;2[q20,r20,s20,t20] + ;3[p1,q1,r1,s1,t1] + ;4[a1,b1,c1,d1] + ;1[f19,g19,h19,i19] + ;2[o19,p19] + ;3[m1,l2,m2,n2,o2] + ;4[e2,f2] + ;1[j18,k18,l18,l19,m19] + ;2[n20] + ;3[h2,i2,i3,j3,k3] + ;4[g1] + ;1[o17,n18,o18,p18,q18] + ;3[d2,d3,e3,f3,g3] + ;1[n13,o13,n14,n15,n16] + ;3[p3,p4,p5,p6] + ;1[n10,n11,o11,p11,p12] + ;3[l4,m4,m5,n5] + ;1[o7,p7,q7,o8,o9] + ;3[j5,k5] + ;1[l6,m6,n6,m7,m8] + ;3[a3,a4,b4,c4] + ;1[i6,j6,j7,k7,j8] + ;3[d5,e5,f5] + ;1[g6,f7,g7,h7] + ;3[j1] + ;1[c6,d6,e6,c7] + ;1[a8,b8,b9,c9] + ;1[d10,e10,d11,e11] + ;1[f9,g9,h9] + ;1[r4,s4,r5,r6,s6] + ;1[t7,s8,t8,r9,s9] + ;1[q13,r13,p14,q14,r14] + ;1[s16,r17,s17,t17,s18] + ;1[l9,k10,l10] + ;1[j11,j12] + ;1[i10] + ) + */ + unique_ptr bd(new Board(Variant::classic_2)); + play(*bd, Color(0), "a20,b20,c20,d20,e20"); + play(*bd, Color(1), "q20,r20,s20,t20"); + play(*bd, Color(2), "p1,q1,r1,s1,t1"); + play(*bd, Color(3), "a1,b1,c1,d1"); + play(*bd, Color(0), "f19,g19,h19,i19"); + play(*bd, Color(1), "o19,p19"); + play(*bd, Color(2), "m1,l2,m2,n2,o2"); + play(*bd, Color(3), "e2,f2"); + play(*bd, Color(0), "j18,k18,l18,l19,m19"); + play(*bd, Color(1), "n20"); + play(*bd, Color(2), "h2,i2,i3,j3,k3"); + play(*bd, Color(3), "g1"); + play(*bd, Color(0), "o17,n18,o18,p18,q18"); + play(*bd, Color(2), "d2,d3,e3,f3,g3"); + play(*bd, Color(0), "n13,o13,n14,n15,n16"); + play(*bd, Color(2), "p3,p4,p5,p6"); + play(*bd, Color(0), "n10,n11,o11,p11,p12"); + play(*bd, Color(2), "l4,m4,m5,n5"); + play(*bd, Color(0), "o7,p7,q7,o8,o9"); + play(*bd, Color(2), "j5,k5"); + play(*bd, Color(0), "l6,m6,n6,m7,m8"); + play(*bd, Color(2), "a3,a4,b4,c4"); + play(*bd, Color(0), "i6,j6,j7,k7,j8"); + play(*bd, Color(2), "d5,e5,f5"); + play(*bd, Color(0), "g6,f7,g7,h7"); + play(*bd, Color(2), "j1"); + play(*bd, Color(0), "c6,d6,e6,c7"); + play(*bd, Color(0), "a8,b8,b9,c9"); + play(*bd, Color(0), "d10,e10,d11,e11"); + play(*bd, Color(0), "f9,g9,h9"); + play(*bd, Color(0), "r4,s4,r5,r6,s6"); + play(*bd, Color(0), "t7,s8,t8,r9,s9"); + play(*bd, Color(0), "q13,r13,p14,q14,r14"); + play(*bd, Color(0), "s16,r17,s17,t17,s18"); + play(*bd, Color(0), "l9,k10,l10"); + play(*bd, Color(0), "j11,j12"); + play(*bd, Color(0), "i10"); + LIBBOARDGAME_CHECK_EQUAL(bd->get_nu_moves(), 37u); + LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(0)), ScoreType(109)); + LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(1)), ScoreType(7)); + LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(2)), ScoreType(38)); + LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(3)), ScoreType(7)); + LIBBOARDGAME_CHECK_EQUAL(bd->get_score(Color(0)), ScoreType(133)); + LIBBOARDGAME_CHECK_EQUAL(bd->get_score(Color(1)), ScoreType(-133)); + LIBBOARDGAME_CHECK_EQUAL(bd->get_score(Color(2)), ScoreType(133)); + LIBBOARDGAME_CHECK_EQUAL(bd->get_score(Color(3)), ScoreType(-133)); + LIBBOARDGAME_CHECK_EQUAL(bd->get_nu_onboard_pieces(Color(0)), 21u); + LIBBOARDGAME_CHECK_EQUAL(bd->get_nu_onboard_pieces(Color(1)), 3u); + LIBBOARDGAME_CHECK_EQUAL(bd->get_nu_onboard_pieces(Color(2)), 10u); + LIBBOARDGAME_CHECK_EQUAL(bd->get_nu_onboard_pieces(Color(3)), 3u); +} + +LIBBOARDGAME_TEST_CASE(pentobi_base_board_gen_moves_classic_initial) +{ + unique_ptr bd(new Board(Variant::classic)); + unique_ptr moves(new MoveList); + unique_ptr marker(new MoveMarker); + bd->gen_moves(Color(0), *marker, *moves); + LIBBOARDGAME_CHECK_EQUAL(moves->size(), 58u); +} + +/** Test get_place() in a 4-color, 2-player game when the player 1 has + a higher score but color 1 has less points than color 2. */ +LIBBOARDGAME_TEST_CASE(pentobi_base_board_get_place) +{ + unique_ptr bd(new Board(Variant::classic_2)); + play(*bd, Color(0), "a20,b20"); + play(*bd, Color(1), "r20,s20,t20"); + play(*bd, Color(2), "q1,r1,s1,t1"); + play(*bd, Color(3), "a1,b1"); + // Not a final position but Board::get_place() should not care about that + unsigned place; + bool isPlaceShared; + bd->get_place(Color(0), place, isPlaceShared); + LIBBOARDGAME_CHECK_EQUAL(place, 0u); + LIBBOARDGAME_CHECK(! isPlaceShared); + bd->get_place(Color(1), place, isPlaceShared); + LIBBOARDGAME_CHECK_EQUAL(place, 1u); + LIBBOARDGAME_CHECK(! isPlaceShared); + bd->get_place(Color(2), place, isPlaceShared); + LIBBOARDGAME_CHECK_EQUAL(place, 0u); + LIBBOARDGAME_CHECK(! isPlaceShared); + bd->get_place(Color(3), place, isPlaceShared); + LIBBOARDGAME_CHECK_EQUAL(place, 1u); + LIBBOARDGAME_CHECK(! isPlaceShared); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libpentobi_base/BoardUpdaterTest.cpp b/src/unittest/libpentobi_base/BoardUpdaterTest.cpp new file mode 100644 index 0000000..f20d7db --- /dev/null +++ b/src/unittest/libpentobi_base/BoardUpdaterTest.cpp @@ -0,0 +1,104 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libpentobi_base/BoardUpdaterTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libpentobi_base/BoardUpdater.h" + +#include "libboardgame_sgf/SgfUtil.h" +#include "libboardgame_sgf/TreeReader.h" +#include "libboardgame_test/Test.h" + +using namespace std; +using namespace libpentobi_base; +using libboardgame_sgf::TreeReader; +using libboardgame_sgf::util::get_last_node; + +//----------------------------------------------------------------------------- + +/** Test that BoardUpdater throws an exception if a piece is played twice. + A tree from a file written by another application could contain move + sequences where a piece is played twice. This could break assumptions + about the maximum number of moves in a game at some places in Pentobi's + code, so BoardUpdater should detect this and throw an exception. */ +LIBBOARDGAME_TEST_CASE(pentobi_base_board_updater_piece_played_twice) +{ + istringstream in("(;GM[Blokus];1[a1];1[a3])"); + TreeReader reader; + reader.read(in); + unique_ptr root = reader.get_tree_transfer_ownership(); + PentobiTree tree(root); + unique_ptr bd(new Board(tree.get_variant())); + BoardUpdater updater; + auto& node = get_last_node(tree.get_root()); + LIBBOARDGAME_CHECK_THROW(updater.update(*bd, tree, node), runtime_error); +} + +/** Test BoardUpdater with setup properties in root node. */ +LIBBOARDGAME_TEST_CASE(pentobi_base_board_updater_setup) +{ + istringstream in("(;GM[Blokus Duo]" + "AB[e8,e9,f9,d10,e10][g6,f7,g7,h7,g8]" + "AW[i4,h5,i5,j5,i6][j7,j8,j9,k9,j10])"); + TreeReader reader; + reader.read(in); + unique_ptr root = reader.get_tree_transfer_ownership(); + PentobiTree tree(root); + unique_ptr bd(new Board(tree.get_variant())); + BoardUpdater updater; + updater.update(*bd, tree, tree.get_root()); + LIBBOARDGAME_CHECK_EQUAL(bd->get_nu_moves(), 0u); + LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(0)), ScoreType(10)); + LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(1)), ScoreType(10)); +} + +/** Test BoardUpdater with setup properties in an inner node. */ +LIBBOARDGAME_TEST_CASE(pentobi_base_board_updater_setup_inner_node) +{ + istringstream in("(;GM[Blokus Duo]" + " ;B[e8,e9,f9,d10,e10]" + " ;AB[g6,f7,g7,h7,g8]AW[i4,h5,i5,j5,i6]" + " ;W[j7,j8,j9,k9,j10])"); + TreeReader reader; + reader.read(in); + unique_ptr root = reader.get_tree_transfer_ownership(); + PentobiTree tree(root); + unique_ptr bd(new Board(tree.get_variant())); + BoardUpdater updater; + auto& node = get_last_node(tree.get_root()); + updater.update(*bd, tree, node); + // BoardUpdater merges setup properties with existing position, so + // get_nu_moves() should return the number of moves played after the setup + LIBBOARDGAME_CHECK_EQUAL(bd->get_nu_moves(), 1u); + LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(0)), ScoreType(10)); + LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(1)), ScoreType(10)); +} + +/** Test removing a piece with the AE property. */ +LIBBOARDGAME_TEST_CASE(pentobi_base_board_updater_setup_empty) +{ + istringstream in("(;GM[Blokus Duo]" + " ;B[e8,e9,f9,d10,e10]" + " ;W[j7,j8,j9,k9,j10]" + " ;AE[e8,e9,f9,d10,e10])"); + TreeReader reader; + reader.read(in); + unique_ptr root = reader.get_tree_transfer_ownership(); + PentobiTree tree(root); + unique_ptr bd(new Board(tree.get_variant())); + BoardUpdater updater; + auto& node = get_last_node(tree.get_root()); + updater.update(*bd, tree, node); + // BoardUpdater merges setup properties with existing position, so + // get_nu_moves() should return the number of moves played after the setup + LIBBOARDGAME_CHECK_EQUAL(bd->get_nu_moves(), 0u); + LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(0)), ScoreType(0)); + LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(1)), ScoreType(5)); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libpentobi_base/CMakeLists.txt b/src/unittest/libpentobi_base/CMakeLists.txt new file mode 100644 index 0000000..1930c00 --- /dev/null +++ b/src/unittest/libpentobi_base/CMakeLists.txt @@ -0,0 +1,19 @@ +add_executable(unittest_libpentobi_base + BoardConstTest.cpp + BoardTest.cpp + BoardUpdaterTest.cpp + GameTest.cpp + TreeTest.cpp +) + +target_link_libraries(unittest_libpentobi_base + boardgame_test_main + pentobi_base + boardgame_base + boardgame_sgf + boardgame_test + boardgame_util + boardgame_sys + ) + +add_test(libpentobi_base unittest_libpentobi_base) diff --git a/src/unittest/libpentobi_base/GameTest.cpp b/src/unittest/libpentobi_base/GameTest.cpp new file mode 100644 index 0000000..e9db78d --- /dev/null +++ b/src/unittest/libpentobi_base/GameTest.cpp @@ -0,0 +1,44 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libpentobi_base/GameTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libpentobi_base/Game.h" + +#include "libboardgame_sgf/SgfUtil.h" +#include "libboardgame_sgf/TreeReader.h" +#include "libboardgame_test/Test.h" + +using namespace std; +using namespace libpentobi_base; +using libboardgame_sgf::TreeReader; +using libboardgame_sgf::util::get_last_node; + +//----------------------------------------------------------------------------- + +/** Test that the current node is in a defined state if the root node contains + invalid properties. */ +LIBBOARDGAME_TEST_CASE(pentobi_base_game_current_defined_invalid_root) +{ + istringstream in("(;GM[Blokus]1[a99999])"); + TreeReader reader; + reader.read(in); + unique_ptr root = reader.get_tree_transfer_ownership(); + Game game(Variant::classic); + try + { + game.init(root); + } + catch (const runtime_error&) + { + // ignore + } + LIBBOARDGAME_CHECK_EQUAL(&game.get_current(), &game.get_root()); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libpentobi_base/TreeTest.cpp b/src/unittest/libpentobi_base/TreeTest.cpp new file mode 100644 index 0000000..1a2270a --- /dev/null +++ b/src/unittest/libpentobi_base/TreeTest.cpp @@ -0,0 +1,112 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libpentobi_base/TreeTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libboardgame_sgf/MissingProperty.h" +#include "libboardgame_sgf/TreeReader.h" +#include "libboardgame_test/Test.h" +#include "libpentobi_base/PentobiTree.h" + +using namespace std; +using namespace libpentobi_base; +using libboardgame_sgf::InvalidPropertyValue; +using libboardgame_sgf::MissingProperty; +using libboardgame_sgf::TreeReader; + +//----------------------------------------------------------------------------- + +/** Check backwards compatibility to move properties used in Pentobi 0.1. + Pentobi 0.1 used the property id's BLUE,YELLOW,RED,GREEN in four-player + game variants instead of 1,2,3,4. (It also used point lists instead of + single-value move properties. */ +LIBBOARDGAME_TEST_CASE(pentobi_base_tree_backward_compatibility_0_1) +{ + istringstream in("(;GM[Blokus Two-Player];BLUE[a16][a17][a18][a19][a20]" + ";YELLOW[s17][t17][t18][t19][t20];RED[t1][t2][t3][t4][t5]" + ";GREEN[a1][b1][c1][d1][d2])"); + TreeReader reader; + reader.read(in); + unique_ptr root = reader.get_tree_transfer_ownership(); + PentobiTree tree(root); + auto& bc = tree.get_board_const(); + auto& geo = bc.get_geometry(); + auto node = &tree.get_root(); + node = &node->get_child(); + { + auto mv = tree.get_move(*node); + LIBBOARDGAME_CHECK(! mv.is_null()); + LIBBOARDGAME_CHECK_EQUAL(mv.color, Color(0)); + auto points = bc.get_move_points(mv.move); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(0, 4))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(0, 3))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(0, 2))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(0, 1))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(0, 0))); + } + node = &node->get_child(); + { + auto mv = tree.get_move(*node); + LIBBOARDGAME_CHECK(! mv.is_null()); + LIBBOARDGAME_CHECK_EQUAL(mv.color, Color(1)); + auto points = bc.get_move_points(mv.move); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(18, 3))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(19, 3))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(19, 2))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(19, 1))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(19, 0))); + } + node = &node->get_child(); + { + auto mv = tree.get_move(*node); + LIBBOARDGAME_CHECK(! mv.is_null()); + LIBBOARDGAME_CHECK_EQUAL(mv.color, Color(2)); + auto points = bc.get_move_points(mv.move); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(19, 19))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(19, 18))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(19, 17))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(19, 16))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(19, 15))); + } + node = &node->get_child(); + { + auto mv = tree.get_move(*node); + LIBBOARDGAME_CHECK(! mv.is_null()); + LIBBOARDGAME_CHECK_EQUAL(mv.color, Color(3)); + auto points = bc.get_move_points(mv.move); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(0, 19))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(1, 19))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(2, 19))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(3, 19))); + LIBBOARDGAME_CHECK(points.contains(geo.get_point(3, 18))); + } +} + +/** Check that Tree constructor throws InvalidPropertyValue on unknown GM + property value. */ +LIBBOARDGAME_TEST_CASE(pentobi_base_tree_invalid_game) +{ + istringstream in("(;GM[1])"); + TreeReader reader; + reader.read(in); + unique_ptr root = reader.get_tree_transfer_ownership(); + LIBBOARDGAME_CHECK_THROW(PentobiTree tree(root), InvalidPropertyValue); +} + +/** Check that Tree constructor throws MissingProperty on missing GM + property. */ +LIBBOARDGAME_TEST_CASE(pentobi_base_tree_missing_game_property) +{ + istringstream in("(;)"); + TreeReader reader; + reader.read(in); + unique_ptr root = reader.get_tree_transfer_ownership(); + LIBBOARDGAME_CHECK_THROW(PentobiTree tree(root), MissingProperty); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libpentobi_mcts/CMakeLists.txt b/src/unittest/libpentobi_mcts/CMakeLists.txt new file mode 100644 index 0000000..3879222 --- /dev/null +++ b/src/unittest/libpentobi_mcts/CMakeLists.txt @@ -0,0 +1,20 @@ +add_executable(unittest_libpentobi_mcts + SearchTest.cpp +) + +target_link_libraries(unittest_libpentobi_mcts + boardgame_test_main + pentobi_mcts + pentobi_base + boardgame_base + boardgame_sgf + boardgame_test + boardgame_util + boardgame_sys + ) + +if(CMAKE_THREAD_LIBS_INIT) + target_link_libraries(unittest_libpentobi_mcts ${CMAKE_THREAD_LIBS_INIT}) +endif() + +add_test(libpentobi_mcts unittest_libpentobi_mcts) diff --git a/src/unittest/libpentobi_mcts/SearchTest.cpp b/src/unittest/libpentobi_mcts/SearchTest.cpp new file mode 100644 index 0000000..ab4333a --- /dev/null +++ b/src/unittest/libpentobi_mcts/SearchTest.cpp @@ -0,0 +1,115 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libpentobi_mcts/SearchTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "libpentobi_mcts/Search.h" + +#include "libboardgame_sgf/SgfUtil.h" +#include "libboardgame_sgf/TreeReader.h" +#include "libboardgame_test/Test.h" +#include "libboardgame_util/CpuTimeSource.h" +#include "libpentobi_base/BoardUpdater.h" +#include "libpentobi_base/PentobiTree.h" + +using namespace std; +using namespace libpentobi_mcts; +using libboardgame_sgf::SgfNode; +using libboardgame_sgf::TreeReader; +using libboardgame_sgf::util::get_last_node; +using libboardgame_util::CpuTimeSource; +using libpentobi_base::BoardUpdater; +using libpentobi_base::PentobiTree; + +//----------------------------------------------------------------------------- + +/** Test that state generates a playout move even if no large pieces are + playable early in the game. + This tests for a bug that occurred in Pentobi 1.1 with game variant Trigon: + Because moves that are below a certain piece size are not generated early + in the game, it could happen in rare cases that no moves were generated + at all. */ +LIBBOARDGAME_TEST_CASE(pentobi_mcts_search_no_large_pieces) +{ + istringstream + in(R"delim( + (;GM[Blokus Trigon Two-Player];1[r4,r5,s5,r6,s6,r7] + ;2[r12,q13,r13,q14,r14,r15];3[k11,l11,m11,n11,j12,k12] + ;4[w7,x7,y7,z7,v8,w8];1[s8,t8,r9,s9,t9,u9] + ;2[n12,o12,m13,n13,o13,o14];3[k13,k14,l14,l15,m15,n15] + ;4[w9,t10,u10,v10,w10,x10];1[n10,o10,p10,q10,r10,r11] + ;2[o15,k16,l16,m16,n16,o16];3[i15,j15,h16,i16,j16,j17] + ;4[u11,s12,t12,u12,v12,v13];1[p4,m5,n5,o5,p5,m6] + ;2[k17,i18,j18,k18,l18,m18];3[l17,m17,n17,o17,p17,o18] + ;4[t14,u14,s15,t15,r16,s16];1[l8,m8,j9,k9,l9,m9]) + )delim"); + TreeReader reader; + reader.read(in); + unique_ptr root = reader.get_tree_transfer_ownership(); + PentobiTree tree(root); + unique_ptr bd(new Board(tree.get_variant())); + BoardUpdater updater; + updater.update(*bd, tree, get_last_node(tree.get_root())); + unsigned nu_threads = 1; + size_t memory = 10000; + unique_ptr search(new Search(bd->get_variant(), nu_threads, + memory)); + Float max_count = 1; + size_t min_simulations = 1; + double max_time = 0; + CpuTimeSource time_source; + Move mv; + bool res = search->search(mv, *bd, Color(1), max_count, min_simulations, + max_time, time_source); + LIBBOARDGAME_CHECK(res); + LIBBOARDGAME_CHECK(! mv.is_null()); +} + +/** Test that useless one-piece moves are generated if no other moves exist. + Useless one-piece moves (all neighbors occupied) are not needed during + the search, but the search should still return one if no other legal + moves exist. */ +LIBBOARDGAME_TEST_CASE(pentobi_mcts_search_callisto_useless_one_piece) +{ + istringstream + in(R"delim( + (;GM[Callisto Two-Player];1[k10];2[k7];1[g6];2[g11] + ;1[f7,g7,h7,f8,h8];2[d9,e9,e10,f10,f11];1[c8,d8,e8,c9] + ;2[k8,l8,m8,l9,l10];1[j11,k11,i12,j12];2[h11,i11,h12,h13,i13] + ;1[n9,m10,n10,l11,m11];2[j4,j5,j6,k6];1[j13,h14,i14,j14,j15] + ;2[h3,g4,h4,i4,h5];1[n6,m7,n7,o7,n8];2[f13,g13,f14,g14] + ;1[c10,d10,c11,d11];2[e5,f5,g5,f6];1[l5,m5,l6,m6];2[e6,c7,d7,e7] + ;1[j3,k3,k4,k5];2[h1,i1,h2,i2];1[e11,e12,f12,e13];2[i8,h9,i9,h10] + ;1[b7,a8,b8,a9];2[k12];1[g15,h15,i15,h16];2[l12,m12,k13,l13] + ;1[j8,j9,j10];2[i5,h6,i6,i7];1[g8,g9,g10];2[g2,f3,g3];1[o9,p9,o10] + ;2[d5,c6,d6];1[b9,b10];2[e4,f4];1[o8,p8]) + )delim"); + TreeReader reader; + reader.read(in); + unique_ptr root = reader.get_tree_transfer_ownership(); + PentobiTree tree(root); + unique_ptr bd(new Board(tree.get_variant())); + BoardUpdater updater; + updater.update(*bd, tree, get_last_node(tree.get_root())); + unsigned nu_threads = 1; + size_t memory = 10000; + unique_ptr search(new Search(bd->get_variant(), nu_threads, + memory)); + Float max_count = 1; + size_t min_simulations = 1; + double max_time = 0; + CpuTimeSource time_source; + Move mv; + bool res = search->search(mv, *bd, Color(0), max_count, min_simulations, + max_time, time_source); + LIBBOARDGAME_CHECK(res); + LIBBOARDGAME_CHECK(! mv.is_null()); + LIBBOARDGAME_CHECK(bd->get_move_piece(mv) == bd->get_one_piece()); +} + +//----------------------------------------------------------------------------- diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 0000000..ef13284 --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,20 @@ +# Build the NSIS installer +# We assume dynamic linking and add a custom target that runs makensis and +# uses windeployqt to include the Qt libraries. + +get_target_property(QMAKE Qt5::qmake LOCATION) +find_program(WINDEPLOYQT windeployqt.exe HINTS "${QMAKE}") + +set(X86 "(x86)") +find_program(MAKENSIS makensis + PATHS "$ENV{ProgramFiles}\\NSIS" "$ENV{ProgramFiles${X86}}\\NSIS") + +add_custom_target(nsis + COMMAND ${CMAKE_COMMAND} -E remove_directory deploy + COMMAND ${CMAKE_COMMAND} -E make_directory deploy + COMMAND ${CMAKE_COMMAND} -E copy "$" deploy + COMMAND ${WINDEPLOYQT} --dir deploy --release --no-svg "deploy/pentobi.exe" + COMMAND ${MAKENSIS} install.nsis +) + +configure_file(install.nsis.in install.nsis @ONLY) diff --git a/windows/German.nsh b/windows/German.nsh new file mode 100644 index 0000000..6963e5f --- /dev/null +++ b/windows/German.nsh @@ -0,0 +1,10 @@ +; German translations +; NSIS version 2.46 does not support Unicode yet, so this file needs to be +; encoded in ISO 8859 + +LangString ADD_START_MENU_ENTRY ${LANG_GERMAN} \ + "Eintrag im Startmenü hinzufügen" +LangString CREATE_DESKTOP_SHORTCUT ${LANG_GERMAN} \ + "Desktopverknüpfung erstellen" +LangString INSTALLER_TITLE ${LANG_GERMAN} \ + "Pentobi ${PENTOBI_VERSION} installieren" diff --git a/windows/blksgf.ico b/windows/blksgf.ico new file mode 100644 index 0000000000000000000000000000000000000000..86b99c960cfcc9134d9b608353b3c28471dd5dca GIT binary patch literal 17020 zcmeHP4O|mP79Yd|sVC@hg{vHpD_69q;s+?6fS^?@Rzy_C%UskS_O^s z*e4f*;O=7&lF|dVrZlvs`FNK_geydCq9P_o@CmDjP3H=8rav$Y$kPU;hk?@!Etc;n zaCJ1;sRu?If-#n0{4zlK=NoXr?%OM@9I+YZoWC}?M}Dt}=32!FNtJKUXUJ;=mzqx0 zFK=?db{E;C*v7Yji3L0nw}8$)6S1Btj`FdO-Wb@le2Uhl;>PqAM%6OlT>kvTN?}&) zf!Q--GMEj!XQpfu@4|k&mG4*)GsC0fu9@8sx=$Mw99qy&B(N4+GN21pwB}34%R1|{ zc#ocNk{l9xEWpNgSE;>_x*g&cdt(7C{M!wZ@ebl>u>*I^xnG#fH*Nkm-BV%b(UCfCE`Nx9auMWfc_w=b zrOP-j$D4wR+i5TFS}jexGdY*-6PMkzR%ZBt!psS8h_yY+JytG_>b*Rd+z zKShrKwu~szhRTPEF-~h`N%IqWjLliN`}1NCZuuS4z_(1{Sj$tet-UJjk$pml1z-iEG+Cu|L;1bW;9FvkMn5UYzB~{K?u~+?7GQ zdif`D)bv#8tq`g}I!}1!=GmvbtKCh}HPLyQI@2C6*fS@yd3S+!hhwXBp|Ipe(WoHy zg$r#pS6fQ+ISY1V!AYp%`6P7S#8y^OUVZ;`(j?dPR_RVOZS~@;_D-hGJ-7D%T3y$N zYL~?;7UeAT-8mUQ$0uyyixZrcHHQX);Y46_c(_wQP#}M`r#m*1M571tw=ftE1MNF( zCzs2ykv?QH8Ab~X;1AS)h&eo3gu)Re=mYdcc(GPb>>gyTQQwu zEBX-f8*z2}53zk&{ek!mq|Vqh?#GPogeej15AqvvjsJbezXpE_Hj?89)fdDEQ)i(6 zM1nbu#y#Td9{*(Z=iPU!@prsaeTYo|rq!PhKNR7dZ)uAC3QE&)3Wv{0CP*$?6H2o~+K0=~3PyuI~Ir&mAtV@n8Axhb~N~F z@JC}|F#Oj#e&C*^LZN`&AJGHu;nL^>-#-lw4*m%40gvR~HMC8`_t3*5!=3t{Z8cXl z2n-H^7s&|#kt^Wim;lHichvB~;(%}@;)`VXJ1q)MPB>|Pr-g&?iifxF`Y4ZeYr554 zpW!p{XKz#G2E0!w621td8vkv@f3E|dt;#;x_o>0GQF^z5U4hTm*zX^KHw}Ng2V$jt zHKJVb`6{LTHKL5hz+mGaA(SiGC(0FRAxid%^4nk^_8Ahy9-Si@_7_pX=Wf58LmiZd z+W&#Z+qvP8(%$J%?ZbZd-j^W8mncUFF1&U2QOsd1P^?jWi8AazB4>W0+T(9w(V+C5 znJB;R_&YndE7>Q?uRH!kdqnwl*zX;GXb+Kv1^<_GsDm=?$KQX>UnHX=PJkfvbLdtIZvj$QQ}2)6whtOuXQ literal 0 HcmV?d00001 diff --git a/windows/install.nsis.in b/windows/install.nsis.in new file mode 100644 index 0000000..12ef4e9 --- /dev/null +++ b/windows/install.nsis.in @@ -0,0 +1,143 @@ +; Script for creating a Windows installer with NSIS (http://nsis.sf.net) + +!define PENTOBI_VERSION "@PENTOBI_VERSION@" +!define PENTOBI_SRC_DIR "@CMAKE_SOURCE_DIR@" +!define PENTOBI_BUILD_DIR "@CMAKE_BINARY_DIR@" + +!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\Pentobi" + +SetCompressor /SOLID lzma + +!define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\orange-install.ico" +!define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\orange-uninstall.ico" +!define MUI_WELCOMEFINISHPAGE_BITMAP "${NSISDIR}\Contrib\Graphics\Wizard\orange.bmp" +!define MUI_COMPONENTSPAGE_NODESC +!include "MUI.nsh" +!insertmacro MUI_PAGE_WELCOME +!insertmacro MUI_PAGE_LICENSE "${PENTOBI_SRC_DIR}\COPYING" +!insertmacro MUI_PAGE_COMPONENTS +!insertmacro MUI_PAGE_DIRECTORY +!insertmacro MUI_PAGE_INSTFILES +!define MUI_FINISHPAGE_RUN "$INSTDIR\Pentobi.exe" +!insertmacro MUI_PAGE_FINISH +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES + +!insertmacro MUI_LANGUAGE "English" +!insertmacro MUI_LANGUAGE "German" +!insertmacro MUI_RESERVEFILE_INSTALLOPTIONS + +!define ADD_START_MENU_ENTRY_DEFAULT "Add start menu entry" +!define CREATE_DESKTOP_SHORTCUT_DEFAULT "Create desktop shortcut" +!define INSTALLER_TITLE_DEFAULT "Pentobi ${PENTOBI_VERSION} Installer" +LangString ADD_START_MENU_ENTRY ${LANG_ENGLISH} \ + "${ADD_START_MENU_ENTRY_DEFAULT}" +LangString CREATE_DESKTOP_SHORTCUT ${LANG_ENGLISH} \ + "${CREATE_DESKTOP_SHORTCUT_DEFAULT}" +LangString INSTALLER_TITLE ${LANG_ENGLISH} \ + "${INSTALLER_TITLE_DEFAULT}" +!include "${PENTOBI_SRC_DIR}\windows\German.nsh" + +Name "Pentobi" +Caption "$(INSTALLER_TITLE)" +OutFile "pentobi-${PENTOBI_VERSION}-install.exe" +InstallDir "$PROGRAMFILES\Pentobi" +InstallDirRegKey HKLM "Software\Pentobi" "" +; Set admin level, needed for shortcut removal on Vista +; (http://nsis.sf.net/Shortcuts_removal_fails_on_Windows_Vista) +RequestExecutionLevel admin + +Section + +IfFileExists "$INSTDIR\Uninstall.exe" 0 +2 +ExecWait '"$INSTDIR\Uninstall.exe" /S _?=$INSTDIR' + +SetOutPath "$INSTDIR\translations" +File "${PENTOBI_BUILD_DIR}\src\libpentobi_gui\*.qm" +File "${PENTOBI_BUILD_DIR}\src\pentobi\*.qm" +SetOutPath "$INSTDIR\books" +File "${PENTOBI_SRC_DIR}\src\books\book_*.blksgf" +SetOutPath "$INSTDIR" +File /r "${PENTOBI_SRC_DIR}\src\pentobi\help" +File /oname=COPYING.txt "${PENTOBI_SRC_DIR}\COPYING" +File /oname=Pentobi.exe "${PENTOBI_BUILD_DIR}\windows\deploy\pentobi.exe" +File "${PENTOBI_SRC_DIR}\src\pentobi\pentobi.ico" +SetOutPath "$INSTDIR" +File "${PENTOBI_BUILD_DIR}\windows\deploy\*.dll" +SetOutPath "$INSTDIR\imageformats" +File "${PENTOBI_BUILD_DIR}\windows\deploy\imageformats\*.dll" +SetOutPath "$INSTDIR\platforms" +File "${PENTOBI_BUILD_DIR}\windows\deploy\platforms\*.dll" +SetOutPath "$INSTDIR\translations" +File "${PENTOBI_BUILD_DIR}\windows\deploy\translations\qt_de.qm" + +WriteRegStr HKLM "Software\Pentobi" "" $INSTDIR + +WriteUninstaller $INSTDIR\Uninstall.exe +WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "Pentobi" +WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${PENTOBI_VERSION}" +WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\pentobi.ico" +WriteRegStr HKLM "${UNINST_KEY}" "URLInfoAbout" "http://pentobi.sf.net/" +WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$INSTDIR\Uninstall.exe" + +SetOutPath "$INSTDIR" +File "${PENTOBI_SRC_DIR}\windows\blksgf.ico" + +WriteRegStr HKCR ".blksgf" "" "Pentobi" +WriteRegStr HKCR ".blksgf" "Content Type" "application/x-blokus-sgf" +WriteRegStr HKCR "Pentobi" "" "Blokus Game" +WriteRegStr HKCR "Pentobi\DefaultIcon" "" "$INSTDIR\blksgf.ico" +WriteRegStr HKCR "Pentobi\shell\open\command" "" \ + "$\"$INSTDIR\Pentobi.exe$\" $\"%1$\"" + +WriteRegStr HKCR "MIME\Database\Content Type\application/x-blokus-sgf" \ + "Extension" ".blksgf" + +WriteRegStr HKCR "Applications\Pentobi.exe" "SupportedTypes" ".blksgf" +WriteRegStr HKCR "Applications\Pentobi\shell\open\command" "" \ + "$\"$INSTDIR\Pentobi.exe$\" $\"%1$\"" + +SectionEnd + +Section "$(ADD_START_MENU_ENTRY)" + +SetShellVarContext all +CreateDirectory "$SMPROGRAMS\Games" +CreateShortCut "$SMPROGRAMS\Games\Pentobi.lnk" "$INSTDIR\Pentobi.exe" + +SectionEnd + +Section "$(CREATE_DESKTOP_SHORTCUT)" + +SetShellVarContext all +CreateShortCut "$DESKTOP\Pentobi.lnk" "$INSTDIR\Pentobi.exe" + +SectionEnd + +Section "Uninstall" + +Delete "$INSTDIR\Uninstall.exe" +Delete "$INSTDIR\Pentobi.exe" +Delete "$INSTDIR\COPYING.txt" +Delete "$INSTDIR\pentobi.ico" +Delete "$INSTDIR\blksgf.ico" +Delete "$INSTDIR\*.dll" +RmDir /r "$INSTDIR\books" +RmDir /r "$INSTDIR\translations" +RmDir /r "$INSTDIR\help" +RmDir /r "$INSTDIR\platforms" +RmDir /r "$INSTDIR\imageformats" +RmDir /r "$INSTDIR\plugins" +RmDir "$INSTDIR" + +SetShellVarContext all +Delete "$SMPROGRAMS\Games\Pentobi.lnk" +Delete "$DESKTOP\Pentobi.lnk" + +DeleteRegKey HKLM "Software\Pentobi" +DeleteRegKey HKLM "${UNINST_KEY}" +DeleteRegKey HKCR "Pentobi" +DeleteRegKey HKCR "Applications\Pentobi.exe" +DeleteRegKey HKCR "Applications\Pentobi" + +SectionEnd -- 2.30.2