Import pentobi_12.2.orig.tar.xz
authorJuhani Numminen <juhaninumminen0@gmail.com>
Thu, 5 Jan 2017 20:14:45 +0000 (20:14 +0000)
committerJuhani Numminen <juhaninumminen0@gmail.com>
Thu, 5 Jan 2017 20:14:45 +0000 (20:14 +0000)
[dgit import orig pentobi_12.2.orig.tar.xz]

575 files changed:
.gitignore [new file with mode: 0644]
CMakeLists.txt [new file with mode: 0644]
COPYING [new file with mode: 0644]
INSTALL [new file with mode: 0644]
NEWS [new file with mode: 0644]
README [new file with mode: 0644]
config.h.in [new file with mode: 0644]
data/CMakeLists.txt [new file with mode: 0644]
data/application-x-blokus-sgf-16.svg [new file with mode: 0644]
data/application-x-blokus-sgf-32.svg [new file with mode: 0644]
data/application-x-blokus-sgf-64.svg [new file with mode: 0644]
data/application-x-blokus-sgf.svg [new file with mode: 0644]
data/pentobi-mime.xml [new file with mode: 0644]
data/pentobi.appdata.xml.in [new file with mode: 0644]
data/pentobi.desktop.in [new file with mode: 0755]
data/pentobi.thumbnailer.in [new file with mode: 0644]
doc/CMakeLists.txt [new file with mode: 0644]
doc/blksgf/Pentobi-SGF.html [new file with mode: 0644]
doc/doxygen/.gitignore [new file with mode: 0644]
doc/doxygen/Doxyfile [new file with mode: 0644]
doc/doxygen/footer.html [new file with mode: 0644]
doc/gtp/Pentobi-GTP.html [new file with mode: 0644]
doc/man/CMakeLists.txt [new file with mode: 0644]
doc/man/pentobi-thumbnailer.6.in [new file with mode: 0644]
doc/man/pentobi.6.in [new file with mode: 0644]
src/CMakeLists.txt [new file with mode: 0644]
src/books/CMakeLists.txt [new file with mode: 0644]
src/books/book_callisto.blksgf [new file with mode: 0644]
src/books/book_callisto_2.blksgf [new file with mode: 0644]
src/books/book_callisto_3.blksgf [new file with mode: 0644]
src/books/book_classic.blksgf [new file with mode: 0644]
src/books/book_classic_2.blksgf [new file with mode: 0644]
src/books/book_classic_3.blksgf [new file with mode: 0644]
src/books/book_duo.blksgf [new file with mode: 0644]
src/books/book_junior.blksgf [new file with mode: 0644]
src/books/book_nexos.blksgf [new file with mode: 0644]
src/books/book_nexos_2.blksgf [new file with mode: 0644]
src/books/book_trigon.blksgf [new file with mode: 0644]
src/books/book_trigon_2.blksgf [new file with mode: 0644]
src/books/book_trigon_3.blksgf [new file with mode: 0644]
src/books/pentobi_books.qrc [new file with mode: 0644]
src/convert/CMakeLists.txt [new file with mode: 0644]
src/convert/Main.cpp [new file with mode: 0644]
src/doc_libboardgame.cpp [new file with mode: 0644]
src/doc_mainpage.cpp [new file with mode: 0644]
src/libboardgame_base/CMakeLists.txt [new file with mode: 0644]
src/libboardgame_base/CoordPoint.cpp [new file with mode: 0644]
src/libboardgame_base/CoordPoint.h [new file with mode: 0644]
src/libboardgame_base/Engine.cpp [new file with mode: 0644]
src/libboardgame_base/Engine.h [new file with mode: 0644]
src/libboardgame_base/Geometry.h [new file with mode: 0644]
src/libboardgame_base/GeometryUtil.h [new file with mode: 0644]
src/libboardgame_base/Grid.h [new file with mode: 0644]
src/libboardgame_base/Marker.h [new file with mode: 0644]
src/libboardgame_base/Point.h [new file with mode: 0644]
src/libboardgame_base/PointTransform.h [new file with mode: 0644]
src/libboardgame_base/Rating.cpp [new file with mode: 0644]
src/libboardgame_base/Rating.h [new file with mode: 0644]
src/libboardgame_base/RectGeometry.h [new file with mode: 0644]
src/libboardgame_base/RectTransform.cpp [new file with mode: 0644]
src/libboardgame_base/RectTransform.h [new file with mode: 0644]
src/libboardgame_base/StringRep.cpp [new file with mode: 0644]
src/libboardgame_base/StringRep.h [new file with mode: 0644]
src/libboardgame_base/Transform.cpp [new file with mode: 0644]
src/libboardgame_base/Transform.h [new file with mode: 0644]
src/libboardgame_gtp/Arguments.cpp [new file with mode: 0644]
src/libboardgame_gtp/Arguments.h [new file with mode: 0644]
src/libboardgame_gtp/CMakeLists.txt [new file with mode: 0644]
src/libboardgame_gtp/CmdLine.cpp [new file with mode: 0644]
src/libboardgame_gtp/CmdLine.h [new file with mode: 0644]
src/libboardgame_gtp/CmdLineRange.h [new file with mode: 0644]
src/libboardgame_gtp/Engine.cpp [new file with mode: 0644]
src/libboardgame_gtp/Engine.h [new file with mode: 0644]
src/libboardgame_gtp/Failure.h [new file with mode: 0644]
src/libboardgame_gtp/Response.cpp [new file with mode: 0644]
src/libboardgame_gtp/Response.h [new file with mode: 0644]
src/libboardgame_mcts/Atomic.h [new file with mode: 0644]
src/libboardgame_mcts/CMakeLists.txt [new file with mode: 0644]
src/libboardgame_mcts/LastGoodReply.h [new file with mode: 0644]
src/libboardgame_mcts/Node.h [new file with mode: 0644]
src/libboardgame_mcts/PlayerMove.h [new file with mode: 0644]
src/libboardgame_mcts/SearchBase.h [new file with mode: 0644]
src/libboardgame_mcts/Tree.h [new file with mode: 0644]
src/libboardgame_mcts/TreeUtil.h [new file with mode: 0644]
src/libboardgame_sgf/CMakeLists.txt [new file with mode: 0644]
src/libboardgame_sgf/InvalidPropertyValue.h [new file with mode: 0644]
src/libboardgame_sgf/InvalidTree.h [new file with mode: 0644]
src/libboardgame_sgf/MissingProperty.cpp [new file with mode: 0644]
src/libboardgame_sgf/MissingProperty.h [new file with mode: 0644]
src/libboardgame_sgf/Reader.cpp [new file with mode: 0644]
src/libboardgame_sgf/Reader.h [new file with mode: 0644]
src/libboardgame_sgf/SgfNode.cpp [new file with mode: 0644]
src/libboardgame_sgf/SgfNode.h [new file with mode: 0644]
src/libboardgame_sgf/SgfTree.cpp [new file with mode: 0644]
src/libboardgame_sgf/SgfTree.h [new file with mode: 0644]
src/libboardgame_sgf/SgfUtil.cpp [new file with mode: 0644]
src/libboardgame_sgf/SgfUtil.h [new file with mode: 0644]
src/libboardgame_sgf/TreeReader.cpp [new file with mode: 0644]
src/libboardgame_sgf/TreeReader.h [new file with mode: 0644]
src/libboardgame_sgf/TreeWriter.cpp [new file with mode: 0644]
src/libboardgame_sgf/TreeWriter.h [new file with mode: 0644]
src/libboardgame_sgf/Writer.cpp [new file with mode: 0644]
src/libboardgame_sgf/Writer.h [new file with mode: 0644]
src/libboardgame_sys/CMakeLists.txt [new file with mode: 0644]
src/libboardgame_sys/Compiler.h [new file with mode: 0644]
src/libboardgame_sys/CpuTime.cpp [new file with mode: 0644]
src/libboardgame_sys/CpuTime.h [new file with mode: 0644]
src/libboardgame_sys/Memory.cpp [new file with mode: 0644]
src/libboardgame_sys/Memory.h [new file with mode: 0644]
src/libboardgame_test/CMakeLists.txt [new file with mode: 0644]
src/libboardgame_test/Test.cpp [new file with mode: 0644]
src/libboardgame_test/Test.h [new file with mode: 0644]
src/libboardgame_test_main/CMakeLists.txt [new file with mode: 0644]
src/libboardgame_test_main/Main.cpp [new file with mode: 0644]
src/libboardgame_util/Abort.cpp [new file with mode: 0644]
src/libboardgame_util/Abort.h [new file with mode: 0644]
src/libboardgame_util/ArrayList.h [new file with mode: 0644]
src/libboardgame_util/Assert.cpp [new file with mode: 0644]
src/libboardgame_util/Assert.h [new file with mode: 0644]
src/libboardgame_util/Barrier.cpp [new file with mode: 0644]
src/libboardgame_util/Barrier.h [new file with mode: 0644]
src/libboardgame_util/CMakeLists.txt [new file with mode: 0644]
src/libboardgame_util/CpuTimeSource.cpp [new file with mode: 0644]
src/libboardgame_util/CpuTimeSource.h [new file with mode: 0644]
src/libboardgame_util/FmtSaver.h [new file with mode: 0644]
src/libboardgame_util/IntervalChecker.cpp [new file with mode: 0644]
src/libboardgame_util/IntervalChecker.h [new file with mode: 0644]
src/libboardgame_util/Log.cpp [new file with mode: 0644]
src/libboardgame_util/Log.h [new file with mode: 0644]
src/libboardgame_util/MathUtil.h [new file with mode: 0644]
src/libboardgame_util/Options.cpp [new file with mode: 0644]
src/libboardgame_util/Options.h [new file with mode: 0644]
src/libboardgame_util/RandomGenerator.cpp [new file with mode: 0644]
src/libboardgame_util/RandomGenerator.h [new file with mode: 0644]
src/libboardgame_util/Range.h [new file with mode: 0644]
src/libboardgame_util/Statistics.h [new file with mode: 0644]
src/libboardgame_util/StringUtil.cpp [new file with mode: 0644]
src/libboardgame_util/StringUtil.h [new file with mode: 0644]
src/libboardgame_util/TimeIntervalChecker.cpp [new file with mode: 0644]
src/libboardgame_util/TimeIntervalChecker.h [new file with mode: 0644]
src/libboardgame_util/TimeSource.cpp [new file with mode: 0644]
src/libboardgame_util/TimeSource.h [new file with mode: 0644]
src/libboardgame_util/Timer.cpp [new file with mode: 0644]
src/libboardgame_util/Timer.h [new file with mode: 0644]
src/libboardgame_util/Unused.h [new file with mode: 0644]
src/libboardgame_util/WallTimeSource.cpp [new file with mode: 0644]
src/libboardgame_util/WallTimeSource.h [new file with mode: 0644]
src/libpentobi_base/Board.cpp [new file with mode: 0644]
src/libpentobi_base/Board.h [new file with mode: 0644]
src/libpentobi_base/BoardConst.cpp [new file with mode: 0644]
src/libpentobi_base/BoardConst.h [new file with mode: 0644]
src/libpentobi_base/BoardUpdater.cpp [new file with mode: 0644]
src/libpentobi_base/BoardUpdater.h [new file with mode: 0644]
src/libpentobi_base/BoardUtil.cpp [new file with mode: 0644]
src/libpentobi_base/BoardUtil.h [new file with mode: 0644]
src/libpentobi_base/Book.cpp [new file with mode: 0644]
src/libpentobi_base/Book.h [new file with mode: 0644]
src/libpentobi_base/CMakeLists.txt [new file with mode: 0644]
src/libpentobi_base/CallistoGeometry.cpp [new file with mode: 0644]
src/libpentobi_base/CallistoGeometry.h [new file with mode: 0644]
src/libpentobi_base/Color.cpp [new file with mode: 0644]
src/libpentobi_base/Color.h [new file with mode: 0644]
src/libpentobi_base/ColorMap.h [new file with mode: 0644]
src/libpentobi_base/ColorMove.h [new file with mode: 0644]
src/libpentobi_base/Engine.cpp [new file with mode: 0644]
src/libpentobi_base/Engine.h [new file with mode: 0644]
src/libpentobi_base/Game.cpp [new file with mode: 0644]
src/libpentobi_base/Game.h [new file with mode: 0644]
src/libpentobi_base/Geometry.h [new file with mode: 0644]
src/libpentobi_base/Grid.h [new file with mode: 0644]
src/libpentobi_base/Marker.h [new file with mode: 0644]
src/libpentobi_base/Move.h [new file with mode: 0644]
src/libpentobi_base/MoveInfo.h [new file with mode: 0644]
src/libpentobi_base/MoveList.h [new file with mode: 0644]
src/libpentobi_base/MoveMarker.h [new file with mode: 0644]
src/libpentobi_base/MovePoints.h [new file with mode: 0644]
src/libpentobi_base/NexosGeometry.cpp [new file with mode: 0644]
src/libpentobi_base/NexosGeometry.h [new file with mode: 0644]
src/libpentobi_base/NodeUtil.cpp [new file with mode: 0644]
src/libpentobi_base/NodeUtil.h [new file with mode: 0644]
src/libpentobi_base/PentobiSgfUtil.cpp [new file with mode: 0644]
src/libpentobi_base/PentobiSgfUtil.h [new file with mode: 0644]
src/libpentobi_base/PentobiTree.cpp [new file with mode: 0644]
src/libpentobi_base/PentobiTree.h [new file with mode: 0644]
src/libpentobi_base/PentobiTreeWriter.cpp [new file with mode: 0644]
src/libpentobi_base/PentobiTreeWriter.h [new file with mode: 0644]
src/libpentobi_base/Piece.h [new file with mode: 0644]
src/libpentobi_base/PieceInfo.cpp [new file with mode: 0644]
src/libpentobi_base/PieceInfo.h [new file with mode: 0644]
src/libpentobi_base/PieceMap.h [new file with mode: 0644]
src/libpentobi_base/PieceTransforms.cpp [new file with mode: 0644]
src/libpentobi_base/PieceTransforms.h [new file with mode: 0644]
src/libpentobi_base/PieceTransformsClassic.cpp [new file with mode: 0644]
src/libpentobi_base/PieceTransformsClassic.h [new file with mode: 0644]
src/libpentobi_base/PieceTransformsTrigon.cpp [new file with mode: 0644]
src/libpentobi_base/PieceTransformsTrigon.h [new file with mode: 0644]
src/libpentobi_base/PlayerBase.cpp [new file with mode: 0644]
src/libpentobi_base/PlayerBase.h [new file with mode: 0644]
src/libpentobi_base/Point.h [new file with mode: 0644]
src/libpentobi_base/PointList.h [new file with mode: 0644]
src/libpentobi_base/PointState.cpp [new file with mode: 0644]
src/libpentobi_base/PointState.h [new file with mode: 0644]
src/libpentobi_base/PrecompMoves.h [new file with mode: 0644]
src/libpentobi_base/ScoreUtil.h [new file with mode: 0644]
src/libpentobi_base/Setup.h [new file with mode: 0644]
src/libpentobi_base/StartingPoints.cpp [new file with mode: 0644]
src/libpentobi_base/StartingPoints.h [new file with mode: 0644]
src/libpentobi_base/SymmetricPoints.cpp [new file with mode: 0644]
src/libpentobi_base/SymmetricPoints.h [new file with mode: 0644]
src/libpentobi_base/TreeUtil.cpp [new file with mode: 0644]
src/libpentobi_base/TreeUtil.h [new file with mode: 0644]
src/libpentobi_base/TrigonGeometry.cpp [new file with mode: 0644]
src/libpentobi_base/TrigonGeometry.h [new file with mode: 0644]
src/libpentobi_base/TrigonTransform.cpp [new file with mode: 0644]
src/libpentobi_base/TrigonTransform.h [new file with mode: 0644]
src/libpentobi_base/Variant.cpp [new file with mode: 0644]
src/libpentobi_base/Variant.h [new file with mode: 0644]
src/libpentobi_gui/BoardPainter.cpp [new file with mode: 0644]
src/libpentobi_gui/BoardPainter.h [new file with mode: 0644]
src/libpentobi_gui/CMakeLists.txt [new file with mode: 0644]
src/libpentobi_gui/ComputerColorDialog.cpp [new file with mode: 0644]
src/libpentobi_gui/ComputerColorDialog.h [new file with mode: 0644]
src/libpentobi_gui/GameInfoDialog.cpp [new file with mode: 0644]
src/libpentobi_gui/GameInfoDialog.h [new file with mode: 0644]
src/libpentobi_gui/GuiBoard.cpp [new file with mode: 0644]
src/libpentobi_gui/GuiBoard.h [new file with mode: 0644]
src/libpentobi_gui/GuiBoardUtil.cpp [new file with mode: 0644]
src/libpentobi_gui/GuiBoardUtil.h [new file with mode: 0644]
src/libpentobi_gui/HelpWindow.cpp [new file with mode: 0644]
src/libpentobi_gui/HelpWindow.h [new file with mode: 0644]
src/libpentobi_gui/InitialRatingDialog.cpp [new file with mode: 0644]
src/libpentobi_gui/InitialRatingDialog.h [new file with mode: 0644]
src/libpentobi_gui/LeaveFullscreenButton.cpp [new file with mode: 0644]
src/libpentobi_gui/LeaveFullscreenButton.h [new file with mode: 0644]
src/libpentobi_gui/LineEdit.cpp [new file with mode: 0644]
src/libpentobi_gui/LineEdit.h [new file with mode: 0644]
src/libpentobi_gui/OrientationDisplay.cpp [new file with mode: 0644]
src/libpentobi_gui/OrientationDisplay.h [new file with mode: 0644]
src/libpentobi_gui/PieceSelector.cpp [new file with mode: 0644]
src/libpentobi_gui/PieceSelector.h [new file with mode: 0644]
src/libpentobi_gui/SameHeightLayout.cpp [new file with mode: 0644]
src/libpentobi_gui/SameHeightLayout.h [new file with mode: 0644]
src/libpentobi_gui/ScoreDisplay.cpp [new file with mode: 0644]
src/libpentobi_gui/ScoreDisplay.h [new file with mode: 0644]
src/libpentobi_gui/Util.cpp [new file with mode: 0644]
src/libpentobi_gui/Util.h [new file with mode: 0644]
src/libpentobi_gui/icons/go-home.svg [new file with mode: 0644]
src/libpentobi_gui/icons/go-next.svg [new file with mode: 0644]
src/libpentobi_gui/icons/go-previous.svg [new file with mode: 0644]
src/libpentobi_gui/libpentobi_gui_resources.qrc [new file with mode: 0644]
src/libpentobi_gui/libpentobi_gui_resources_2x.qrc [new file with mode: 0644]
src/libpentobi_gui/translations/libpentobi_gui_de.ts [new file with mode: 0644]
src/libpentobi_kde_thumbnailer/CMakeLists.txt [new file with mode: 0644]
src/libpentobi_mcts/AnalyzeGame.cpp [new file with mode: 0644]
src/libpentobi_mcts/AnalyzeGame.h [new file with mode: 0644]
src/libpentobi_mcts/CMakeLists.txt [new file with mode: 0644]
src/libpentobi_mcts/Float.h [new file with mode: 0644]
src/libpentobi_mcts/History.cpp [new file with mode: 0644]
src/libpentobi_mcts/History.h [new file with mode: 0644]
src/libpentobi_mcts/Player.cpp [new file with mode: 0644]
src/libpentobi_mcts/Player.h [new file with mode: 0644]
src/libpentobi_mcts/PlayoutFeatures.cpp [new file with mode: 0644]
src/libpentobi_mcts/PlayoutFeatures.h [new file with mode: 0644]
src/libpentobi_mcts/PriorKnowledge.cpp [new file with mode: 0644]
src/libpentobi_mcts/PriorKnowledge.h [new file with mode: 0644]
src/libpentobi_mcts/Search.cpp [new file with mode: 0644]
src/libpentobi_mcts/Search.h [new file with mode: 0644]
src/libpentobi_mcts/SearchParamConst.h [new file with mode: 0644]
src/libpentobi_mcts/SharedConst.cpp [new file with mode: 0644]
src/libpentobi_mcts/SharedConst.h [new file with mode: 0644]
src/libpentobi_mcts/State.cpp [new file with mode: 0644]
src/libpentobi_mcts/State.h [new file with mode: 0644]
src/libpentobi_mcts/StateUtil.cpp [new file with mode: 0644]
src/libpentobi_mcts/StateUtil.h [new file with mode: 0644]
src/libpentobi_mcts/Util.cpp [new file with mode: 0644]
src/libpentobi_mcts/Util.h [new file with mode: 0644]
src/libpentobi_thumbnail/CMakeLists.txt [new file with mode: 0644]
src/libpentobi_thumbnail/CreateThumbnail.cpp [new file with mode: 0644]
src/libpentobi_thumbnail/CreateThumbnail.h [new file with mode: 0644]
src/pentobi/AnalyzeGameWidget.cpp [new file with mode: 0644]
src/pentobi/AnalyzeGameWidget.h [new file with mode: 0644]
src/pentobi/AnalyzeGameWindow.cpp [new file with mode: 0644]
src/pentobi/AnalyzeGameWindow.h [new file with mode: 0644]
src/pentobi/AnalyzeSpeedDialog.cpp [new file with mode: 0644]
src/pentobi/AnalyzeSpeedDialog.h [new file with mode: 0644]
src/pentobi/Application.cpp [new file with mode: 0644]
src/pentobi/Application.h [new file with mode: 0644]
src/pentobi/CMakeLists.txt [new file with mode: 0644]
src/pentobi/Main.cpp [new file with mode: 0644]
src/pentobi/MainWindow.cpp [new file with mode: 0644]
src/pentobi/MainWindow.h [new file with mode: 0644]
src/pentobi/RatedGamesList.cpp [new file with mode: 0644]
src/pentobi/RatedGamesList.h [new file with mode: 0644]
src/pentobi/RatingDialog.cpp [new file with mode: 0644]
src/pentobi/RatingDialog.h [new file with mode: 0644]
src/pentobi/RatingGraph.cpp [new file with mode: 0644]
src/pentobi/RatingGraph.h [new file with mode: 0644]
src/pentobi/RatingHistory.cpp [new file with mode: 0644]
src/pentobi/RatingHistory.h [new file with mode: 0644]
src/pentobi/ShowMessage.cpp [new file with mode: 0644]
src/pentobi/ShowMessage.h [new file with mode: 0644]
src/pentobi/Util.cpp [new file with mode: 0644]
src/pentobi/Util.h [new file with mode: 0644]
src/pentobi/help/C/pentobi/analysis.jpg [new file with mode: 0644]
src/pentobi/help/C/pentobi/become_stronger.html [new file with mode: 0644]
src/pentobi/help/C/pentobi/board_callisto.png [new file with mode: 0644]
src/pentobi/help/C/pentobi/board_classic.png [new file with mode: 0644]
src/pentobi/help/C/pentobi/board_duo.png [new file with mode: 0644]
src/pentobi/help/C/pentobi/board_nexos.png [new file with mode: 0644]
src/pentobi/help/C/pentobi/board_trigon.jpg [new file with mode: 0644]
src/pentobi/help/C/pentobi/callisto_rules.html [new file with mode: 0644]
src/pentobi/help/C/pentobi/classic_rules.html [new file with mode: 0644]
src/pentobi/help/C/pentobi/duo_rules.html [new file with mode: 0644]
src/pentobi/help/C/pentobi/index.html [new file with mode: 0644]
src/pentobi/help/C/pentobi/junior_rules.html [new file with mode: 0644]
src/pentobi/help/C/pentobi/license.html [new file with mode: 0644]
src/pentobi/help/C/pentobi/nexos_rules.html [new file with mode: 0644]
src/pentobi/help/C/pentobi/pieces.png [new file with mode: 0644]
src/pentobi/help/C/pentobi/pieces_callisto.png [new file with mode: 0644]
src/pentobi/help/C/pentobi/pieces_junior.png [new file with mode: 0644]
src/pentobi/help/C/pentobi/pieces_nexos.png [new file with mode: 0644]
src/pentobi/help/C/pentobi/pieces_trigon.jpg [new file with mode: 0644]
src/pentobi/help/C/pentobi/position_callisto.png [new file with mode: 0644]
src/pentobi/help/C/pentobi/position_classic.png [new file with mode: 0644]
src/pentobi/help/C/pentobi/position_duo.png [new file with mode: 0644]
src/pentobi/help/C/pentobi/position_nexos.png [new file with mode: 0644]
src/pentobi/help/C/pentobi/position_trigon.jpg [new file with mode: 0644]
src/pentobi/help/C/pentobi/rating.jpg [new file with mode: 0644]
src/pentobi/help/C/pentobi/shortcuts.html [new file with mode: 0644]
src/pentobi/help/C/pentobi/stylesheet.css [new file with mode: 0644]
src/pentobi/help/C/pentobi/system.html [new file with mode: 0644]
src/pentobi/help/C/pentobi/trigon_rules.html [new file with mode: 0644]
src/pentobi/help/C/pentobi/user_interface.html [new file with mode: 0644]
src/pentobi/help/C/pentobi/window_menu.html [new file with mode: 0644]
src/pentobi/help/de/pentobi/become_stronger.html [new file with mode: 0644]
src/pentobi/help/de/pentobi/callisto_rules.html [new file with mode: 0644]
src/pentobi/help/de/pentobi/classic_rules.html [new file with mode: 0644]
src/pentobi/help/de/pentobi/duo_rules.html [new file with mode: 0644]
src/pentobi/help/de/pentobi/index.html [new file with mode: 0644]
src/pentobi/help/de/pentobi/junior_rules.html [new file with mode: 0644]
src/pentobi/help/de/pentobi/license.html [new file with mode: 0644]
src/pentobi/help/de/pentobi/nexos_rules.html [new file with mode: 0644]
src/pentobi/help/de/pentobi/shortcuts.html [new file with mode: 0644]
src/pentobi/help/de/pentobi/system.html [new file with mode: 0644]
src/pentobi/help/de/pentobi/trigon_rules.html [new file with mode: 0644]
src/pentobi/help/de/pentobi/user_interface.html [new file with mode: 0644]
src/pentobi/help/de/pentobi/window_menu.html [new file with mode: 0644]
src/pentobi/icons.qrc [new file with mode: 0644]
src/pentobi/icons/pentobi-16.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-32.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-64.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-backward-16.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-backward.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-beginning-16.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-beginning.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-computer-colors-16.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-computer-colors.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-end-16.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-end.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-flip-horizontal.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-flip-vertical.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-forward-16.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-forward.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-newgame-16.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-newgame.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-next-piece.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-next-variation-16.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-next-variation.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-piece-clear.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-play-16.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-play.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-previous-piece.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-previous-variation-16.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-previous-variation.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-rated-game-16.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-rated-game.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-rotate-left.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-rotate-right.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-undo-16.svg [new file with mode: 0644]
src/pentobi/icons/pentobi-undo.svg [new file with mode: 0644]
src/pentobi/icons/pentobi.svg [new file with mode: 0644]
src/pentobi/pentobi.conf.in [new file with mode: 0644]
src/pentobi/pentobi.ico [new file with mode: 0644]
src/pentobi/pentobi.rc [new file with mode: 0644]
src/pentobi/resources.qrc [new file with mode: 0644]
src/pentobi/resources_2x.qrc [new file with mode: 0644]
src/pentobi/translations/pentobi.ts [new file with mode: 0644]
src/pentobi/translations/pentobi_de.ts [new file with mode: 0644]
src/pentobi_gtp/CMakeLists.txt [new file with mode: 0644]
src/pentobi_gtp/Engine.cpp [new file with mode: 0644]
src/pentobi_gtp/Engine.h [new file with mode: 0644]
src/pentobi_gtp/Main.cpp [new file with mode: 0644]
src/pentobi_kde_thumbnailer/CMakeLists.txt [new file with mode: 0644]
src/pentobi_kde_thumbnailer/PentobiThumbCreator.cpp [new file with mode: 0644]
src/pentobi_kde_thumbnailer/PentobiThumbCreator.h [new file with mode: 0644]
src/pentobi_kde_thumbnailer/pentobi-thumbnail.desktop [new file with mode: 0644]
src/pentobi_qml/.gitignore [new file with mode: 0644]
src/pentobi_qml/CMakeLists.txt [new file with mode: 0644]
src/pentobi_qml/GameModel.cpp [new file with mode: 0644]
src/pentobi_qml/GameModel.h [new file with mode: 0644]
src/pentobi_qml/Main.cpp [new file with mode: 0644]
src/pentobi_qml/Pentobi.pro [new file with mode: 0644]
src/pentobi_qml/PieceModel.cpp [new file with mode: 0644]
src/pentobi_qml/PieceModel.h [new file with mode: 0644]
src/pentobi_qml/PlayerModel.cpp [new file with mode: 0644]
src/pentobi_qml/PlayerModel.h [new file with mode: 0644]
src/pentobi_qml/android/AndroidManifest.xml [new file with mode: 0644]
src/pentobi_qml/android/res/drawable-hdpi/icon.png [new file with mode: 0644]
src/pentobi_qml/android/res/drawable-mdpi/icon.png [new file with mode: 0644]
src/pentobi_qml/android/res/drawable/splash.xml [new file with mode: 0644]
src/pentobi_qml/android/res/values/theme.xml [new file with mode: 0644]
src/pentobi_qml/android_icons_svg/icon48.svg [new file with mode: 0644]
src/pentobi_qml/android_icons_svg/icon72.svg [new file with mode: 0644]
src/pentobi_qml/deployment.pri [new file with mode: 0644]
src/pentobi_qml/icons_android.qrc [new file with mode: 0644]
src/pentobi_qml/qml/.gitignore [new file with mode: 0644]
src/pentobi_qml/qml/AndroidToolBar.qml [new file with mode: 0644]
src/pentobi_qml/qml/AndroidToolButton.qml [new file with mode: 0644]
src/pentobi_qml/qml/Board.qml [new file with mode: 0644]
src/pentobi_qml/qml/Button.qml [new file with mode: 0644]
src/pentobi_qml/qml/ComputerColorDialog.qml [new file with mode: 0644]
src/pentobi_qml/qml/GameDisplay.js [new file with mode: 0644]
src/pentobi_qml/qml/GameDisplay.qml [new file with mode: 0644]
src/pentobi_qml/qml/LineSegment.qml [new file with mode: 0644]
src/pentobi_qml/qml/Main.js [new file with mode: 0644]
src/pentobi_qml/qml/Main.qml [new file with mode: 0644]
src/pentobi_qml/qml/MenuComputer.qml [new file with mode: 0644]
src/pentobi_qml/qml/MenuEdit.qml [new file with mode: 0644]
src/pentobi_qml/qml/MenuGame.qml [new file with mode: 0644]
src/pentobi_qml/qml/MenuGo.qml [new file with mode: 0644]
src/pentobi_qml/qml/MenuHelp.qml [new file with mode: 0644]
src/pentobi_qml/qml/MenuItemGameVariant.qml [new file with mode: 0644]
src/pentobi_qml/qml/MenuItemLevel.qml [new file with mode: 0644]
src/pentobi_qml/qml/MenuView.qml [new file with mode: 0644]
src/pentobi_qml/qml/NavigationPanel.qml [new file with mode: 0644]
src/pentobi_qml/qml/OpenDialog.qml [new file with mode: 0644]
src/pentobi_qml/qml/PieceCallisto.qml [new file with mode: 0644]
src/pentobi_qml/qml/PieceClassic.qml [new file with mode: 0644]
src/pentobi_qml/qml/PieceFlipAnimation.qml [new file with mode: 0644]
src/pentobi_qml/qml/PieceList.qml [new file with mode: 0644]
src/pentobi_qml/qml/PieceManipulator.qml [new file with mode: 0644]
src/pentobi_qml/qml/PieceNexos.qml [new file with mode: 0644]
src/pentobi_qml/qml/PieceRotationAnimation.qml [new file with mode: 0644]
src/pentobi_qml/qml/PieceSelector.qml [new file with mode: 0644]
src/pentobi_qml/qml/PieceTrigon.qml [new file with mode: 0644]
src/pentobi_qml/qml/SaveDialog.qml [new file with mode: 0644]
src/pentobi_qml/qml/ScoreDisplay.qml [new file with mode: 0644]
src/pentobi_qml/qml/ScoreElement.qml [new file with mode: 0644]
src/pentobi_qml/qml/ScoreElement2.qml [new file with mode: 0644]
src/pentobi_qml/qml/Square.qml [new file with mode: 0644]
src/pentobi_qml/qml/ToolBar.qml [new file with mode: 0644]
src/pentobi_qml/qml/Triangle.qml [new file with mode: 0644]
src/pentobi_qml/qml/i18n/qml_de.ts [new file with mode: 0644]
src/pentobi_qml/qml/i18n/replace_qtbase_de.ts [new file with mode: 0644]
src/pentobi_qml/qml/icons/menu.svg [new file with mode: 0644]
src/pentobi_qml/qml/icons/pentobi-backward.svg [new file with mode: 0644]
src/pentobi_qml/qml/icons/pentobi-beginning.svg [new file with mode: 0644]
src/pentobi_qml/qml/icons/pentobi-computer-colors.svg [new file with mode: 0644]
src/pentobi_qml/qml/icons/pentobi-end.svg [new file with mode: 0644]
src/pentobi_qml/qml/icons/pentobi-forward.svg [new file with mode: 0644]
src/pentobi_qml/qml/icons/pentobi-newgame.svg [new file with mode: 0644]
src/pentobi_qml/qml/icons/pentobi-next-variation.svg [new file with mode: 0644]
src/pentobi_qml/qml/icons/pentobi-play.svg [new file with mode: 0644]
src/pentobi_qml/qml/icons/pentobi-previous-variation.svg [new file with mode: 0644]
src/pentobi_qml/qml/icons/pentobi-undo.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/dark/Theme.qml [new file with mode: 0644]
src/pentobi_qml/qml/themes/dark/board-callisto-2.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/dark/board-callisto-3.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/dark/board-callisto.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/dark/board-tile-classic.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/dark/board-tile-nexos.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/dark/board-trigon-3.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/dark/board-trigon.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/Theme.qml [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/board-callisto-2.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/board-callisto-3.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/board-callisto.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/board-tile-classic.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/board-tile-nexos.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/board-trigon-3.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/board-trigon.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/frame-blue.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/frame-green.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/frame-red.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/frame-yellow.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/junction-all-blue.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/junction-all-green.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/junction-all-red.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/junction-all-yellow.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/junction-rect-blue.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/junction-rect-green.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/junction-rect-red.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/junction-rect-yellow.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/junction-straight-blue.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/junction-straight-green.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/junction-straight-red.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/junction-straight-yellow.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/junction-t-blue.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/junction-t-green.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/junction-t-red.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/junction-t-yellow.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/linesegment-blue.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/linesegment-green.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/linesegment-red.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/linesegment-yellow.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/piece-manipulator-legal.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/piece-manipulator.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/square-blue.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/square-green.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/square-red.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/square-yellow.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/triangle-blue.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/triangle-down-blue.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/triangle-down-green.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/triangle-down-red.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/triangle-down-yellow.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/triangle-green.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/triangle-red.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/light/triangle-yellow.svg [new file with mode: 0644]
src/pentobi_qml/qml/themes/theme_dark.qrc [new file with mode: 0644]
src/pentobi_qml/qml/themes/theme_light.qrc [new file with mode: 0644]
src/pentobi_qml/qml/themes/theme_shared.qrc [new file with mode: 0644]
src/pentobi_qml/resources.qrc [new file with mode: 0644]
src/pentobi_qml/translations.qrc [new file with mode: 0644]
src/pentobi_thumbnailer/CMakeLists.txt [new file with mode: 0644]
src/pentobi_thumbnailer/Main.cpp [new file with mode: 0644]
src/twogtp/Analyze.cpp [new file with mode: 0644]
src/twogtp/Analyze.h [new file with mode: 0644]
src/twogtp/CMakeLists.txt [new file with mode: 0644]
src/twogtp/FdStream.cpp [new file with mode: 0644]
src/twogtp/FdStream.h [new file with mode: 0644]
src/twogtp/GtpConnection.cpp [new file with mode: 0644]
src/twogtp/GtpConnection.h [new file with mode: 0644]
src/twogtp/Main.cpp [new file with mode: 0644]
src/twogtp/Output.cpp [new file with mode: 0644]
src/twogtp/Output.h [new file with mode: 0644]
src/twogtp/OutputTree.cpp [new file with mode: 0644]
src/twogtp/OutputTree.h [new file with mode: 0644]
src/twogtp/TwoGtp.cpp [new file with mode: 0644]
src/twogtp/TwoGtp.h [new file with mode: 0644]
src/unittest/CMakeLists.txt [new file with mode: 0644]
src/unittest/libboardgame_base/CMakeLists.txt [new file with mode: 0644]
src/unittest/libboardgame_base/MarkerTest.cpp [new file with mode: 0644]
src/unittest/libboardgame_base/PointTransformTest.cpp [new file with mode: 0644]
src/unittest/libboardgame_base/RatingTest.cpp [new file with mode: 0644]
src/unittest/libboardgame_base/RectGeometryTest.cpp [new file with mode: 0644]
src/unittest/libboardgame_base/StringRepTest.cpp [new file with mode: 0644]
src/unittest/libboardgame_gtp/ArgumentsTest.cpp [new file with mode: 0644]
src/unittest/libboardgame_gtp/CMakeLists.txt [new file with mode: 0644]
src/unittest/libboardgame_gtp/CmdLineTest.cpp [new file with mode: 0644]
src/unittest/libboardgame_gtp/EngineTest.cpp [new file with mode: 0644]
src/unittest/libboardgame_gtp/ResponseTest.cpp [new file with mode: 0644]
src/unittest/libboardgame_mcts/CMakeLists.txt [new file with mode: 0644]
src/unittest/libboardgame_mcts/NodeTest.cpp [new file with mode: 0644]
src/unittest/libboardgame_sgf/CMakeLists.txt [new file with mode: 0644]
src/unittest/libboardgame_sgf/SgfNodeTest.cpp [new file with mode: 0644]
src/unittest/libboardgame_sgf/SgfUtilTest.cpp [new file with mode: 0644]
src/unittest/libboardgame_sgf/TreeReaderTest.cpp [new file with mode: 0644]
src/unittest/libboardgame_util/ArrayListTest.cpp [new file with mode: 0644]
src/unittest/libboardgame_util/CMakeLists.txt [new file with mode: 0644]
src/unittest/libboardgame_util/OptionsTest.cpp [new file with mode: 0644]
src/unittest/libboardgame_util/StatisticsTest.cpp [new file with mode: 0644]
src/unittest/libboardgame_util/StringUtilTest.cpp [new file with mode: 0644]
src/unittest/libpentobi_base/BoardConstTest.cpp [new file with mode: 0644]
src/unittest/libpentobi_base/BoardTest.cpp [new file with mode: 0644]
src/unittest/libpentobi_base/BoardUpdaterTest.cpp [new file with mode: 0644]
src/unittest/libpentobi_base/CMakeLists.txt [new file with mode: 0644]
src/unittest/libpentobi_base/GameTest.cpp [new file with mode: 0644]
src/unittest/libpentobi_base/TreeTest.cpp [new file with mode: 0644]
src/unittest/libpentobi_mcts/CMakeLists.txt [new file with mode: 0644]
src/unittest/libpentobi_mcts/SearchTest.cpp [new file with mode: 0644]
windows/CMakeLists.txt [new file with mode: 0644]
windows/German.nsh [new file with mode: 0644]
windows/blksgf.ico [new file with mode: 0644]
windows/install.nsis.in [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..2b21e9e
--- /dev/null
@@ -0,0 +1,2 @@
+CMakeLists.txt.user
+Pentobi.creator.user
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644 (file)
index 0000000..ff848ce
--- /dev/null
@@ -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 (file)
index 0000000..72591e1
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,694 @@
+Copyright (C) 2011-2017 Markus Enzenberger <enz@users.sourceforge.net>
+
+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. <http://fsf.org/>
+ 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.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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 <http://www.gnu.org/licenses/>.
+
+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:
+
+    <program>  Copyright (C) <year>  <name of author>
+    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
+<http://www.gnu.org/licenses/>.
+
+  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
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/INSTALL b/INSTALL
new file mode 100644 (file)
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 (file)
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 (file)
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 <enz@users.sourceforge.net>
+
+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 (file)
index 0000000..774b93e
--- /dev/null
@@ -0,0 +1,30 @@
+/* Define to 1 if you have the <unistd.h> header file. */
+#cmakedefine01 HAVE_UNISTD_H
+
+/* Define to 1 if you have the <sys/times.h> header file. */
+#cmakedefine01 HAVE_SYS_TIMES_H
+
+/* Define to 1 if you have the <sys/sysctl.h> 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 (file)
index 0000000..9fef947
--- /dev/null
@@ -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 (file)
index 0000000..d0d9743
--- /dev/null
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <rect x="1.5" y="0.5" width="13" height="15" stroke-linejoin="round" ry="1.15" stroke="#7c7f79" fill="#fff"/>
+ <rect x="3" y="2" width="10" height="12" fill="#eeeeec"/>
+ <rect x="4" y="4" width="4" height="4" fill="#C00"/>
+ <path d="m4 8v-4h4l-1 1h-2v2z" fill="#f14646"/>
+ <rect x="8" y="4" width="4" height="4" fill="#73d216"/>
+ <path d="m8 8v-4h4l-1 1h-2v2z" fill="#b0eb76"/>
+ <rect x="4" y="8" width="4" height="4" fill="#edd400"/>
+ <path d="m4 12v-4h4l-1 1h-2v2z" fill="#fdee76"/>
+ <rect x="8" y="8" width="4" height="4" fill="#3465a4"/>
+ <path d="m8 12v-4h4l-1 1h-2v2z" fill="#6f9dce"/>
+</svg>
diff --git a/data/application-x-blokus-sgf-32.svg b/data/application-x-blokus-sgf-32.svg
new file mode 100644 (file)
index 0000000..935183a
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="32" width="32" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <rect x="4.5" y="2.5" width="23" height="27" stroke-linejoin="round" ry="1.15" stroke="#555753" fill="#fff"/>
+ <rect x="6" y="4" width="20" height="24" fill="#eeeeec"/>
+ <g id="h">
+  <rect x="16" y="8" width="4" fill="#73d216" height="4"/>
+  <path d="m16 12v-4h4l-1 1h-2v2z" fill="#b0eb76"/>
+ </g>
+ <use x="4" xlink:href="#h"/>
+ <use y="4" x="4" xlink:href="#h"/>
+ <use y="8" x="4" xlink:href="#h"/>
+ <g id="g">
+  <rect x="12" y="16" height="4" width="4" fill="#3465a4"/>
+  <path d="m12 20v-4h4l-1 1h-2v2z" fill="#6f9dce"/>
+ </g>
+ <use x="4" xlink:href="#g"/>
+ <use y="4" x="4" xlink:href="#g"/>
+ <use y="4" x="8" xlink:href="#g"/>
+ <g id="f">
+  <rect x="8" y="12" width="4" fill="#edd400" height="4"/>
+  <path d="m8 16v-4h4l-1 1h-2v2z" fill="#fdee76"/>
+ </g>
+ <use y="4" xlink:href="#f"/>
+ <use y="8" xlink:href="#f"/>
+ <use y="8" x="4" xlink:href="#f"/>
+ <g id="e">
+  <rect x="8" y="8" width="4" fill="#C00" height="4"/>
+  <path d="m8 12v-4h4l-1 1h-2v2z" fill="#f14646"/>
+ </g>
+ <use x="4" xlink:href="#e"/>
+ <use y="4" x="4" xlink:href="#e"/>
+ <use y="4" x="8" xlink:href="#e"/>
+</svg>
diff --git a/data/application-x-blokus-sgf-64.svg b/data/application-x-blokus-sgf-64.svg
new file mode 100644 (file)
index 0000000..495a843
--- /dev/null
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="64" width="64" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <rect x="6.5" y="3.5" width="47" height="55" stroke-linejoin="round" ry="1.15" stroke="#555753" fill="#fff"/>
+ <rect x="8" y="5" width="44" height="52" fill="#eeeeec"/>
+ <g id="a">
+ <rect height="7" width="7" x="30" y="18" fill="#73d216"/>
+ <path d="m30 25h7v-7l-1 1v 5h-5z" fill="#4e9a06"/>
+ <path d="m30 25v-7h7l-1 1h-5v5z" fill="#8ae234"/>
+ </g>
+ <use xlink:href="#a" x="7"/>
+ <use xlink:href="#a" x="7" y="7"/>
+ <use xlink:href="#a" x="7" y="14"/>
+ <g id="b">
+ <rect height="7" width="7" x="23" y="32" fill="#3465a4"/>
+ <path d="m23 39h7v-7l-1 1v 5h-5z" fill="#204a87"/>
+ <path d="m23 39v-7h7l-1 1h-5v5z" fill="#558bc5"/>
+ </g>
+ <use xlink:href="#b" x="7"/>
+ <use xlink:href="#b" x="7" y="7"/>
+ <use xlink:href="#b" x="14" y="7"/>
+ <g id="c">
+ <rect height="7" width="7" x="16" y="25" fill="#edd400"/>
+ <path d="m16 32h7v-7l-1 1v 5h-5z" fill="#c4a000"/>
+ <path d="m16 32v-7h7l-1 1h-5v5z" fill="#fce94f"/>
+ </g>
+ <use xlink:href="#c" y="7"/>
+ <use xlink:href="#c" y="14"/>
+ <use xlink:href="#c" x="7" y="14"/>
+ <g id="d">
+ <rect height="7" width="7" x="16" y="18" fill="#C00"/>
+ <path d="m16 25h7v-7l-1 1v 5h-5z" fill="#a40000"/>
+ <path d="m16 25v-7h7l-1 1h-5v5z" fill="#ef2929"/>
+ </g>
+ <use xlink:href="#d" x="7"/>
+ <use xlink:href="#d" x="7" y="7"/>
+ <use xlink:href="#d" x="14" y="7"/>
+</svg>
diff --git a/data/application-x-blokus-sgf.svg b/data/application-x-blokus-sgf.svg
new file mode 100644 (file)
index 0000000..c903262
--- /dev/null
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="48" width="48" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <rect x="6.5" y="3.5" width="35" height="41" stroke-linejoin="round" ry="1.15" stroke="#555753" fill="#fff"/>
+ <rect x="8" y="5" width="32" height="38" fill="#eeeeec"/>
+ <g id="a">
+ <rect height="6" width="6" x="24" y="12" fill="#73d216"/>
+ <path d="m24 18h6v-6l-1 1v4h-4z" fill="#4e9a06"/>
+ <path d="m24 18v-6h6l-1 1h-4v4z" fill="#8ae234"/>
+ </g>
+ <use xlink:href="#a" x="6"/>
+ <use xlink:href="#a" x="6" y="6"/>
+ <use xlink:href="#a" x="6" y="12"/>
+ <g id="b">
+ <rect height="6" width="6" x="18" y="24" fill="#3465a4"/>
+ <path d="m18 30h6v-6l-1 1v4h-4z" fill="#204a87"/>
+ <path d="m18 30v-6h6l-1 1h-4v4z" fill="#558bc5"/>
+ </g>
+ <use xlink:href="#b" x="6"/>
+ <use xlink:href="#b" x="6" y="6"/>
+ <use xlink:href="#b" x="12" y="6"/>
+ <g id="c">
+ <rect height="6" width="6" x="12" y="18" fill="#edd400"/>
+ <path d="m12 24h6v-6l-1 1v4h-4z" fill="#c4a000"/>
+ <path d="m12 24v-6h6l-1 1h-4v4z" fill="#fce94f"/>
+ </g>
+ <use xlink:href="#c" y="6"/>
+ <use xlink:href="#c" y="12"/>
+ <use xlink:href="#c" x="6" y="12"/>
+ <g id="d">
+ <rect height="6" width="6" x="12" y="12" fill="#C00"/>
+ <path d="m12 18h6v-6l-1 1v4h-4z" fill="#a40000"/>
+ <path d="m12 18v-6h6l-1 1h-4v4z" fill="#ef2929"/>
+ </g>
+ <use xlink:href="#d" x="6"/>
+ <use xlink:href="#d" x="6" y="6"/>
+ <use xlink:href="#d" x="12" y="6"/>
+</svg>
diff --git a/data/pentobi-mime.xml b/data/pentobi-mime.xml
new file mode 100644 (file)
index 0000000..fe420d7
--- /dev/null
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<mime-info xmlns='http://www.freedesktop.org/standards/shared-mime-info'>
+<mime-type type="application/x-blokus-sgf">
+<comment>Blokus game</comment>
+<comment xml:lang="de">Blokus-Partie</comment>
+<magic priority="60">
+<match type="string" offset="0:256" value="GM[Blokus]"/>
+<match type="string" offset="0:256" value="GM[Blokus Duo]"/>
+<match type="string" offset="0:256" value="GM[Blokus Junior]"/>
+<match type="string" offset="0:256" value="GM[Blokus Trigon]"/>
+<match type="string" offset="0:256" value="GM[Blokus Trigon Three-Player]"/>
+<match type="string" offset="0:256" value="GM[Blokus Trigon Two-Player]"/>
+<match type="string" offset="0:256" value="GM[Blokus Two-Player]"/>
+<match type="string" offset="0:256" value="GM[Callisto]"/>
+<match type="string" offset="0:256" value="GM[Callisto Three-Player]"/>
+<match type="string" offset="0:256" value="GM[Callisto Two-Player]"/>
+<match type="string" offset="0:256" value="GM[Nexos]"/>
+<match type="string" offset="0:256" value="GM[Nexos Two-Player]"/>
+</magic>
+<sub-class-of type="text/plain"/>
+<glob pattern="*.blksgf"/>
+</mime-type>
+</mime-info>
diff --git a/data/pentobi.appdata.xml.in b/data/pentobi.appdata.xml.in
new file mode 100644 (file)
index 0000000..c3c691b
--- /dev/null
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="utf-8"?>
+<component type="desktop">
+  <id>pentobi.desktop</id>
+  <metadata_license>CC0-1.0</metadata_license>
+  <project_license>GPL-3.0+</project_license>
+  <name>Pentobi</name>
+  <name xml:lang="de">Pentobi</name>
+  <summary>Computer opponent for the board game Blokus</summary>
+  <summary xml:lang="de">Computer-Gegner für das Brettspiel Blokus</summary>
+
+  <description>
+    <p>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.</p>
+    <p xml:lang="de">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.</p>
+
+    <p>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.</p>
+    <p xml:lang="de">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.</p>
+
+    <p>System requirements: 1 GB RAM, 1 GHz CPU (4 GB RAM, 2 GHz dual-core or
+    faster CPU recommended for playing level 9).</p>
+    <p xml:lang="de">Systemminima: 1 GB RAM, 1 GHz CPU (4 GB RAM, 2 GHz
+    Dual-Core- oder schnellere CPU empfohlen für Spielstufe 9).</p>
+
+    <p>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.</p>
+    <p xml:lang="de">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.</p>
+  </description>
+
+  <screenshots>
+    <screenshot type="default">
+      <image width="1248" height="702">
+      http://pentobi.sourceforge.net/pentobi-classic.png</image>
+      <caption>Game variant Classic</caption>
+      <caption xml:lang="de">Spielvariante Klassisch</caption>
+    </screenshot>
+    <screenshot>
+      <image width="1248" height="702">
+      http://pentobi.sourceforge.net/pentobi-duo.png</image>
+      <caption>Game variant Duo</caption>
+      <caption xml:lang="de">Spielvariante Duo</caption>
+    </screenshot>
+    <screenshot>
+      <image width="1248" height="702">
+      http://pentobi.sourceforge.net/pentobi-trigon.png</image>
+      <caption>Game variant Trigon</caption>
+      <caption xml:lang="de">Spielvariante Trigon</caption>
+    </screenshot>
+    <screenshot>
+      <image width="1248" height="702">
+      http://pentobi.sourceforge.net/pentobi-nexos.png</image>
+      <caption>Game variant Nexos</caption>
+      <caption xml:lang="de">Spielvariante Nexos</caption>
+    </screenshot>
+    <screenshot>
+      <image width="1248" height="702">
+      http://pentobi.sourceforge.net/pentobi-callisto.png</image>
+      <caption>Game variant Callisto</caption>
+      <caption xml:lang="de">Spielvariante Callisto</caption>
+    </screenshot>
+  </screenshots>
+
+  <url type="homepage">http://pentobi.sourceforge.net/</url>
+  <url type="bugtracker">https://sourceforge.net/p/pentobi/bugs/</url>
+  <url type="donation">https://sourceforge.net/p/pentobi/donate/</url>
+  <developer_name>Markus Enzenberger</developer_name>
+  <update_contact>enz@users.sourceforge.net</update_contact>
+
+  <provides>
+    <binary>pentobi</binary>
+  </provides>
+  <mimetypes>
+    <mimetype>application/x-blokus-sgf</mimetype>
+  </mimetypes>
+
+  <releases>
+     <release version="@PENTOBI_VERSION@" date="@PENTOBI_RELEASE_DATE@"/>
+  </releases>
+</component>
diff --git a/data/pentobi.desktop.in b/data/pentobi.desktop.in
new file mode 100755 (executable)
index 0000000..0992025
--- /dev/null
@@ -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 (file)
index 0000000..dd95f90
--- /dev/null
@@ -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 (file)
index 0000000..5cf3005
--- /dev/null
@@ -0,0 +1 @@
+add_subdirectory(man)
diff --git a/doc/blksgf/Pentobi-SGF.html b/doc/blksgf/Pentobi-SGF.html
new file mode 100644 (file)
index 0000000..76b40a9
--- /dev/null
@@ -0,0 +1,147 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+<head>
+<title>Pentobi SGF Files</title>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<h1>Pentobi SGF Files</h1>
+<div style="font-size:small">Author: Markus Enzenberger<br>
+Last modified: 2016-11-27</div>
+<p>This document describes the file format for <a href=
+"http://en.wikipedia.org/wiki/Blokus">Blokus</a> game records as used by the
+program <a href="http://pentobi.sourceforge.net">Pentobi</a>. The most recent
+version of this document can be found in the source code distribution of
+Pentobi in the folder pentobi/doc/blksgf.</p>
+<h2>Introduction</h2>
+<p>The file format is a derivative of the <a href=
+"http://www.red-bean.com/sgf/">Smart Game Format</a> (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 <a href="http://www.red-bean.com/sgf/ff5/ff5.htm">discussions</a>
+about the future SGF version 5.</p>
+<h2>File Extension and MIME Type</h2>
+<p>The file extension <tt>.blksgf</tt> and the <a href=
+"http://en.wikipedia.org/wiki/Internet_media_type">MIME type</a>
+<tt>application/x-blokus-sgf</tt> are used for Blokus SGF files.</p>
+<p style="font-size:small"><b>Note</b><br>
+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 <a href=
+"http://en.wikipedia.org/wiki/.htaccess">.htaccess</a> 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:</p>
+<blockquote style="font-size:small">AddType application/x-blokus-sgf
+blksgf</blockquote>
+<h2>Character Set</h2>
+<p>Although not specific to Blokus, it is recommended to use <a href=
+"http://en.wikipedia.org/wiki/UTF-8">UTF-8</a> as the character set. Pentobi
+always writes files in UTF-8 and indicates that with the <tt>CA</tt> 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 <tt>CA</tt> property.</p>
+<h2>Game Property</h2>
+<p>Since there is no number for Blokus defined in SGF 4, a string instead of a
+number is used as the value for the <tt>GM</tt> 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.</p>
+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.
+<h2>Color and Player Properties</h2>
+<p>In game variants with two players and two colors, <tt>B</tt> denotes the
+first player or color, <tt>W</tt> the second player or color. In game variants
+with three or four players and one color per player, <tt>1</tt>, <tt>2</tt>,
+<tt>3</tt>, <tt>4</tt> denote the first, second, third, and fourth player or
+color. In game variants with two players and four colors, <tt>B</tt> denotes
+the first player, <tt>W</tt> the second player, and <tt>1</tt>, <tt>2</tt>,
+<tt>3</tt>, <tt>4</tt> denote the first, second, third, and fourth color. This
+applies to move properties and properties related to a player or a color.</p>
+<p>Example 1: in the game variant Blokus Two-Player <tt>PW</tt> is the name of
+the first player, and <tt>1</tt> is a move of the first color.</p>
+<p>Example 2: in the game variant Blokus Two-Player, one could either use the
+<tt>BL</tt>, <tt>WL</tt> 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
+<tt>1L</tt>, <tt>2L</tt>, <tt>3L</tt>, <tt>4L</tt> 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.)</p>
+<p style="font-size:small"><b>Note</b><br>
+Pentobi versions before 0.2 used the properties <tt>BLUE</tt>, <tt>YELLOW</tt>,
+<tt>RED</tt>, <tt>GREEN</tt> 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.</p>
+<h2>Coordinate System</h2>
+<p>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.</p>
+<p>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.</p>
+<p>For Trigon, hexagonal boards are mapped to rectangular coordinates as in the
+following example of a hexagon with edge size 3:</p>
+<pre>
+       6     / \ / \ / \ / \
+       5   / \ / \ / \ / \ / \
+       4 / \ / \ / \ / \ / \ / \
+       3 \ / \ / \ / \ / \ / \ /
+       2   \ / \ / \ / \ / \ /
+       1     \ / \ / \ / \ /
+          a b c d e f g h i j k
+</pre>
+<p>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:</p>
+<pre>
+       6 |   |   |
+       5 + - + - + -
+       4 |   |   |
+       3 + - + - + -
+       2 |   |   |
+       1 + - + - + -
+         a b c d e f
+</pre>
+<h2>Move Properties</h2>
+<p>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.</p>
+<p>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.</p>
+<p>Example: <tt>B[f9,e10,f10,g10,f11]</tt></p>
+<p>In Nexos, moves contain only the coordinates of line segments occupied by
+the piece, no coordinates of junctions.</p>
+<p style="font-size:small"><b>Note</b><br>
+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.</p>
+<h2>Setup Properties</h2>
+<p>The setup properties <tt>AB</tt>, <tt>AW</tt>, <tt>A1</tt>, <tt>A2</tt>,
+<tt>A3</tt>, <tt>A4</tt> can be used to place several pieces simultaneously on
+the board. The setup property <tt>AE</tt> 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 <tt>PL</tt> can be used
+to set the color to play in a setup position.</p>
+<p>Example:<br>
+<tt>AB[e8,e9,f9,d10,e10][g6,f7,g7,h7,g8]<br>
+AW[i4,h5,i5,j5,i6][j7,j8,j9,k9,j10]<br>
+PL[B]</tt></p>
+<p style="font-size:small"><b>Note</b><br>
+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.</p>
+</body>
+</html>
diff --git a/doc/doxygen/.gitignore b/doc/doxygen/.gitignore
new file mode 100644 (file)
index 0000000..1b0a5aa
--- /dev/null
@@ -0,0 +1,2 @@
+doxygen_sqlite3.db
+html/
diff --git a/doc/doxygen/Doxyfile b/doc/doxygen/Doxyfile
new file mode 100644 (file)
index 0000000..326986f
--- /dev/null
@@ -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 (file)
index 0000000..13af170
--- /dev/null
@@ -0,0 +1,8 @@
+<p>
+<hr>
+<div style="text-align:right;">
+$date <a href="http://www.doxygen.org/">Doxygen</a> $doxygenversion
+</div>
+</p>
+</body>
+</html>
diff --git a/doc/gtp/Pentobi-GTP.html b/doc/gtp/Pentobi-GTP.html
new file mode 100644 (file)
index 0000000..6864272
--- /dev/null
@@ -0,0 +1,321 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+<head>
+<title>Pentobi GTP Interface</title>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<h1>Pentobi GTP Interface</h1>
+<div style="font-size:small">Author: Markus Enzenberger</div>
+<p>This document describes the text-based interface to the engine of the Blokus
+program <a href="http://pentobi.sourceforge.net">Pentobi</a>. The interface is
+an adaption of the <a href="http://www.lysator.liu.se/~gunnar/gtp/">Go Text
+Protocol</a> (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.</p>
+<h2>Go Text Protocol</h2>
+<p>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: <tt>=</tt> for success, <tt>?</tt> for failure, followed by
+the actual response. The response ends with two consecutive newline characters.
+See the <a href=
+"http://www.lysator.liu.se/~gunnar/gtp/gtp2-spec-draft2/gtp2-spec.html">GTP
+specification</a> for details.</p>
+<h2>Controllers</h2>
+<p>To use the engine from a controller program, the controller typically
+creates a child process by running <tt>pentobi-gtp</tt> 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
+<tt>java.lang.Runtime.exec()</tt>.</p>
+<p>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 <tt>pentobi-gtp</tt> 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 <tt>pentobi-gtp</tt> with
+the command line option <tt>--quiet</tt>, but it is generally better to assume
+that a GTP engine writes text to standard error.</p>
+<p>An example for a controller written in C++ for Linux is included in Pentobi
+since version 9.0 in <tt>src/twogtp</tt>. 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
+<tt>tools/twogtp/twogtp.py</tt>.</p>
+<h2>Building</h2>
+<p>Since the GTP engine is a developer tool, building it is not enabled by
+default. To enable it, run <tt>cmake</tt> with the option
+<tt>-DPENTOBI_BUILD_GTP=ON</tt>. After building, there will be an executable in
+the build directory named <tt>src/pentobi_gtp/pentobi-gtp</tt>. 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
+<tt>-DPENTOBI_BUILD_GUI=OFF</tt>.</p>
+<h2>Options</h2>
+<p>The following command-line options are supported by
+<tt>pentobi-gtp</tt>:</p>
+<dl>
+<dt>--book <i>file</i></dt>
+<dd>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
+<tt>src/books</tt>). If no opening book is specified and opening books are not
+disabled, <tt>pentobi-gtp</tt> 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 <tt>src/books</tt>. If no such file is found it
+will print an error message to standard error and disable the use of opening
+books.</dd>
+<dt>--config,-c <i>file</i></dt>
+<dd>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).</dd>
+<dt>--color</dt>
+<dd>Use ANSI escape sequences to colorize the text output of boards (for
+example in the response to the <tt>showboard</tt> command or with the
+--showboard command line option).</dd>
+<dt>--cputime</dt>
+<dd>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.</dd>
+<dt>--game,-g <i>variant</i></dt>
+<dd>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.</dd>
+<dt>--help,-h</dt>
+<dd>Print a list of the command-line options and exit.</dd>
+<dt>--level,-l <i>n</i></dt>
+<dd>Set the level of playing strength to n. Valid values are 1 to 9.</dd>
+<dt>--seed,-r <i>n</i></dt>
+<dd>Use <i>n</i> 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.</dd>
+<dt>--showboard</dt>
+<dd>Automatically write a text representation of the current position to
+standard error after each command that alters the position.</dd>
+<dt>--nobook</dt>
+<dd>Disable the use of opening books.</dd>
+<dt>--noresign</dt>
+<dd>Disable resignation. If resignation is desabled, the <tt>genmove</tt>
+command will never respond with <tt>resign</tt>. Resignation can speed up the
+playing of test games if only the win/loss information is wanted.</dd>
+<dt>--quiet,-q</dt>
+<dd>Do not print any debugging messages, errors or warnings to standard
+error.</dd>
+<dt>--threads <i>n</i></dt>
+<dd>Use <i>n</i> 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.</dd>
+<dt>--version,-v</dt>
+<dd>Print the version of Pentobi and exit.</dd>
+</dl>
+<h2>Commands</h2>
+<h3>Standard Commands</h3>
+<p>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 (<tt>B</tt>, <tt>W</tt> if
+two players or colors; <tt>1</tt>, <tt>2</tt>, <tt>3</tt>, <tt>4</tt> if more
+than two). Moves in arguments or responses are represented as in the move
+property values of blksgf files. See the specification for <a href=
+"http://pentobi.sourceforge.net/Pentobi-SGF.html">Pentobi SGF files</a> for
+details.</p>
+<dl>
+<dt>all_legal <i>color</i></dt>
+<dd>List all legal moves for a color.</dd>
+<dt>clear_board</dt>
+<dd>Clear the board and start a new game in the current game variant.</dd>
+<dt>final_score</dt>
+<dd>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. <tt>B+2</tt> if the first player wins with two points, or
+<tt>0</tt> for a draw). In game variants with more than two players, the
+response is a list of the points for each player (e.g.
+<tt>64&nbsp;69&nbsp;70&nbsp;40</tt>). If the current position is not a final
+position, the response is undefined.</dd>
+<dt>genmove <i>color</i></dt>
+<dd>Generate and play a move for a given color in the current position. If the
+color has no more moves, the response is <tt>pass</tt>. If resignation is not
+disabled, the response is <tt>resign</tt> if the players is very likely to
+lose. Otherwise the response is the move.</dd>
+<dt>known_command <i>command</i></dt>
+<dd>The response is <tt>true</tt> if <i>command</i> is a GTP command supported
+by the engine, <tt>false</tt> otherwise.</dd>
+<dt>list_commands</dt>
+<dd>List all supported GTP commands, one command per line.</dd>
+<dt>loadsgf <i>file</i> [<i>move_number</i>]</dt>
+<dd>Load a board position from a blksgf file with name <i>file</i>. If
+<i>move_number</i> is specified, the board position will be set to the position
+in the main variation of the file <u>before</u> the move with the given number
+was played, otherwise to the last position in the main variation.</dd>
+<dt>name</dt>
+<dd>Return the name of the GTP engine (<tt>Pentobi</tt>).</dd>
+<dt>play <i>color</i> <i>move</i></dt>
+<dd>Play a move for a given color in the current board position.</dd>
+<dt>protocol_version</dt>
+<dd>Return the version of the GTP protocol used (currently <tt>2</tt>).</dd>
+<dt>quit</dt>
+<dd>Exit the command loop and quit the engine.</dd>
+<dt>reg_genmove <i>color</i></dt>
+<dd>Like the <tt>genmove</tt> command, but only generates a move and does not
+play it on the board.</dd>
+<dt>showboard</dt>
+<dd>Return a text representation of the current board position.</dd>
+<dt>undo</dt>
+<dd>Undo the last move played.</dd>
+<dt>version</dt>
+<dd>Return the version of Pentobi.</dd>
+</dl>
+<h3>Generally Useful Extension Commands</h3>
+<dl>
+<dt>cputime</dt>
+<dd>Return the CPU time used by the engine since the start of the program.</dd>
+<dt>cputime_diff</dt>
+<dd>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.</dd>
+<dt>g</dt>
+<dd>Shortcut for the <tt>genmove</tt> command with the color argument set to
+the current color to play.</dd>
+<dt>get_place <i>color</i></dt>
+<dd>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 <tt>shared</tt> is
+appended to the place number.</dd>
+<dt>get_value</dt>
+<dd>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
+<tt>reg_genmove</tt> or <tt>genmove</tt> 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 <tt>get_value</tt> command is
+used.</dd>
+<dt>p <i>move</i></dt>
+<dd>Shortcut for the <tt>play</tt> command with the color argument set to the
+current color to play.</dd>
+<dt>param [<i>key</i> <i>value</i>]</dt>
+<dd>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:
+<blockquote>
+<dl>
+<dt>avoid_symmetric_draw 0|1</dt>
+<dd>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 <tt>1</tt>) by default.</dd>
+<dt>fixed_simulations <i>n</i></dt>
+<dd>Use exactly <i>n</i> 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.</dd>
+<dt>use_book 0|1</dt>
+<dd>Enable or disable the opening book.</dd>
+</dl>
+</blockquote>
+The other parameters are only interesting for developers.</dd>
+<dt>param_base [<i>key</i> <i>value</i>]</dt>
+<dd>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.
+<blockquote>
+<dl>
+<dt>accept_illegal 0|1</dt>
+<dd>Accept move arguments to the <tt>play</tt> command that violate the rules
+of the game. If disabled, the <tt>play</tt> command will respond with an error,
+otherwise it will perform the moves.</dd>
+<dt>resign 0|1</dt>
+<dd>Allow the engine to respond with <tt>resign</tt> to the <tt>genmove</tt>
+command.</dd>
+</dl>
+</blockquote>
+</dd>
+<dt>set_game <i>variant</i></dt>
+<dd>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.
+<tt>Blokus&nbsp;Duo</tt>, see the specification for <a href=
+"http://pentobi.sourceforge.net/Pentobi-SGF.html">Pentobi SGF files</a> for
+details).</dd>
+<dt>set_random_seed <i>n</i></dt>
+<dd>Set the seed of the random generator to <i>n</i>. See the documentation for
+the command-line option --seed.</dd>
+</dl>
+<h3>Extension Commands for Developers</h3>
+The remaining commands are only interesting for developers. See Pentobi's
+source code for details.
+<h2>Example</h2>
+<p>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.</p>
+<pre>
+<i>$ ./pentobi-gtp --quiet</i>
+<b>name</b>
+= Pentobi
+
+<b>version</b>
+= 7.1
+
+<b>set_game Blokus Duo</b>
+=
+
+<b>play b e8,d9,e9,f9,e10</b>
+=
+
+<b>genmove w</b>
+= i4,h5,i5,j5,i6
+
+<b>showboard</b>
+=
+   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 . . . . . . . .&gt;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
+
+<b>quit</b>
+=
+
+</pre>
+</body>
+</html>
diff --git a/doc/man/CMakeLists.txt b/doc/man/CMakeLists.txt
new file mode 100644 (file)
index 0000000..d85259e
--- /dev/null
@@ -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 (file)
index 0000000..a82c26d
--- /dev/null
@@ -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 <enz@users.sourceforge.net>
diff --git a/doc/man/pentobi.6.in b/doc/man/pentobi.6.in
new file mode 100644 (file)
index 0000000..a7ce12b
--- /dev/null
@@ -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 <enz@users.sourceforge.net>
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
new file mode 100644 (file)
index 0000000..4f2ccf2
--- /dev/null
@@ -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 (file)
index 0000000..d440a5a
--- /dev/null
@@ -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 (file)
index 0000000..c9bccbf
--- /dev/null
@@ -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 (file)
index 0000000..14f40fa
--- /dev/null
@@ -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 (file)
index 0000000..6109c33
--- /dev/null
@@ -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 (file)
index 0000000..bf9ad9e
--- /dev/null
@@ -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 (file)
index 0000000..debcbc7
--- /dev/null
@@ -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 (file)
index 0000000..273d56e
--- /dev/null
@@ -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 (file)
index 0000000..6559619
--- /dev/null
@@ -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 (file)
index 0000000..9d86c8e
--- /dev/null
@@ -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 (file)
index 0000000..7793fc6
--- /dev/null
@@ -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 (file)
index 0000000..e535a52
--- /dev/null
@@ -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 (file)
index 0000000..da0292e
--- /dev/null
@@ -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 (file)
index 0000000..99b9f7e
--- /dev/null
@@ -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 (file)
index 0000000..7de2ba6
--- /dev/null
@@ -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 (file)
index 0000000..3adda93
--- /dev/null
@@ -0,0 +1,17 @@
+<RCC>
+    <qresource prefix="/pentobi_books">
+        <file>book_callisto.blksgf</file>
+        <file>book_callisto_2.blksgf</file>
+        <file>book_callisto_3.blksgf</file>
+        <file>book_classic.blksgf</file>
+        <file>book_classic_2.blksgf</file>
+        <file>book_classic_3.blksgf</file>
+        <file>book_duo.blksgf</file>
+        <file>book_junior.blksgf</file>
+        <file>book_nexos.blksgf</file>
+        <file>book_nexos_2.blksgf</file>
+        <file>book_trigon.blksgf</file>
+        <file>book_trigon_2.blksgf</file>
+        <file>book_trigon_3.blksgf</file>
+    </qresource>
+</RCC>
diff --git a/src/convert/CMakeLists.txt b/src/convert/CMakeLists.txt
new file mode 100644 (file)
index 0000000..20bddab
--- /dev/null
@@ -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 (file)
index 0000000..6ba8a50
--- /dev/null
@@ -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 <iostream>
+#include <QCommandLineParser>
+#include <QCoreApplication>
+#include <QImageReader>
+#include <QImageWriter>
+
+//-----------------------------------------------------------------------------
+
+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 (file)
index 0000000..0081b4d
--- /dev/null
@@ -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.
+<a href="http://webdocs.cs.ualberta.ca/~mmueller/ps/enzenberger-mueller-acg12.pdf">(PDF)</a>
+
+@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.
+<a href="http://www.machinelearning.org/proceedings/icml2007/papers/387.pdf">(PDF)</a>
+
+@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
+<a href="http://www.sztaki.hu/~szcsaba/papers/ecml06.pdf">(PDF)</a>
+
+*/
diff --git a/src/doc_mainpage.cpp b/src/doc_mainpage.cpp
new file mode 100644 (file)
index 0000000..978b7a1
--- /dev/null
@@ -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
+    <a href="http://qt.digia.com/">Qt</a> 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
+      <a href="http://www.gnome.org/">Gnome</a> desktop
+    - pentobi_kde_thumbnailer -
+      Plugin for file preview thumbnails for the
+      <a href="http://www.kde.org/">KDE</a> 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 (file)
index 0000000..d51513d
--- /dev/null
@@ -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 (file)
index 0000000..da120fe
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "CoordPoint.h"
+
+#include <iostream>
+
+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 (file)
index 0000000..6761272
--- /dev/null
@@ -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 <limits>
+#include <iosfwd>
+
+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<int>::max(), numeric_limits<int>::max());
+}
+
+inline bool CoordPoint::is_onboard(int x, int y, unsigned width,
+                                   unsigned height)
+{
+    return x >= 0 && x < static_cast<int>(width)
+            && y >= 0 && y < static_cast<int>(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<int>::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 (file)
index 0000000..195f849
--- /dev/null
@@ -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 <config.h>
+#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 <br>
+    Arguments: random seed */
+void Engine::cmd_set_random_seed(const Arguments& args)
+{
+    RandomGenerator::set_global_seed(args.parse<RandomGenerator::ResultType>());
+}
+
+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 (file)
index 0000000..4498260
--- /dev/null
@@ -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 (file)
index 0000000..27bdc48
--- /dev/null
@@ -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 <memory>
+#include <sstream>
+#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 P>
+class Geometry
+{
+public:
+    typedef P Point;
+
+    typedef typename Point::IntType IntType;
+
+    /** On-board adjacent neighbors of a point. */
+    typedef ArrayList<Point, 4, unsigned short> AdjList;
+
+    /** On-board diagonal neighbors of a point
+        Currently supports up to nine diagonal points as used on boards
+        for Blokus Trigon. */
+    typedef ArrayList<Point, 9, unsigned short> DiagList;
+
+    /** Adjacent neighbors of a coordinate. */
+    typedef ArrayList<CoordPoint, 4> AdjCoordList;
+
+    /** Diagonal neighbors of a coordinate. */
+    typedef ArrayList<CoordPoint, 9> 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<StringRep> string_rep =
+                          unique_ptr<StringRep>(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<StringRep> 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<class P>
+Geometry<P>::Geometry(unique_ptr<StringRep> string_rep)
+    : m_string_rep(move(string_rep))
+{ }
+
+template<class P>
+Geometry<P>::~Geometry() = default;
+
+template<class P>
+bool Geometry<P>::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<class P>
+inline auto Geometry<P>::get_adj(Point p) const -> const AdjList&
+{
+    LIBBOARDGAME_ASSERT(is_valid(p));
+    return m_adj[p.to_int()];
+}
+
+template<class P>
+inline auto Geometry<P>::get_diag(Point p) const -> const DiagList&
+{
+    LIBBOARDGAME_ASSERT(is_valid(p));
+    return m_diag[p.to_int()];
+}
+
+template<class P>
+inline unsigned Geometry<P>::get_height() const
+{
+    return m_height;
+}
+
+template<class P>
+inline auto Geometry<P>::get_point(unsigned x, unsigned y) const -> Point
+{
+    LIBBOARDGAME_ASSERT(x < m_width);
+    LIBBOARDGAME_ASSERT(y < m_height);
+    return m_points[x][y];
+}
+
+template<class P>
+inline unsigned Geometry<P>::get_point_type(Point p) const
+{
+    LIBBOARDGAME_ASSERT(is_valid(p));
+    return m_point_type[p.to_int()];
+}
+
+template<class P>
+inline unsigned Geometry<P>::get_point_type(CoordPoint p) const
+{
+    return get_point_type(p.x, p.y);
+}
+
+template<class P>
+inline auto Geometry<P>::get_range() const -> IntType
+{
+    return m_range;
+}
+
+template<class P>
+inline unsigned Geometry<P>::get_width() const
+{
+    return m_width;
+}
+
+template<class P>
+inline unsigned Geometry<P>::get_x(Point p) const
+{
+    LIBBOARDGAME_ASSERT(is_valid(p));
+    return m_x[p.to_int()];
+}
+
+template<class P>
+inline unsigned Geometry<P>::get_y(Point p) const
+{
+    LIBBOARDGAME_ASSERT(is_valid(p));
+    return m_y[p.to_int()];
+}
+
+template<class P>
+void Geometry<P>::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<class P>
+inline bool Geometry<P>::is_onboard(unsigned x, unsigned y) const
+{
+    return ! get_point(x, y).is_null();
+}
+
+template<class P>
+bool Geometry<P>::is_onboard(CoordPoint p) const
+{
+    return p.is_onboard(m_width, m_height) && is_onboard(p.x, p.y);
+}
+
+#if LIBBOARDGAME_DEBUG
+
+template<class P>
+inline bool Geometry<P>::is_valid(Point p) const
+{
+    return ! p.is_null() && p.to_int() < get_range();
+}
+
+#endif
+
+template<class P>
+inline const string& Geometry<P>::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 (file)
index 0000000..99ff8f2
--- /dev/null
@@ -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<typename T>
+void normalize_offset(T begin, T end, unsigned& width, unsigned& height,
+                      CoordPoint& offset)
+{
+    int min_x = numeric_limits<int>::max();
+    int min_y = numeric_limits<int>::max();
+    int max_x = numeric_limits<int>::min();
+    int max_y = numeric_limits<int>::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<typename P>
+CoordPoint type_match_offset(const Geometry<P>& 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<typename P, typename T>
+void type_match_shift(const Geometry<P>& 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 (file)
index 0000000..1e1edd1
--- /dev/null
@@ -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 <algorithm>
+#include <cstring>
+#include <iomanip>
+#include <sstream>
+#include <type_traits>
+#include "Geometry.h"
+
+namespace libboardgame_base {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+template<class T>
+string grid_to_string(const T& grid, const Geometry<typename T::Point>& 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 P, typename T> 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 P, typename T>
+class Grid
+{
+    friend class GridExt<P, T>; // for GridExt::copy_from(Grid)
+
+public:
+    typedef P Point;
+
+    typedef libboardgame_base::Geometry<P> 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<T>::value */
+    void memcpy_from(const Grid& grid, const Geometry& geo);
+
+private:
+    T m_a[Point::range_onboard];
+};
+
+template<class P, typename T>
+inline T& Grid<P, T>::operator[](const Point& p)
+{
+    LIBBOARDGAME_ASSERT(! p.is_null());
+    return m_a[p.to_int()];
+}
+
+template<class P, typename T>
+inline const T& Grid<P, T>::operator[](const Point& p) const
+{
+    LIBBOARDGAME_ASSERT(! p.is_null());
+    return m_a[p.to_int()];
+}
+
+template<class P, typename T>
+inline void Grid<P, T>::copy_from(const Grid& grid, const Geometry& geo)
+{
+    copy(grid.m_a, grid.m_a + geo.get_range(), m_a);
+}
+
+template<class P, typename T>
+inline void Grid<P, T>::fill(const T& val, const Geometry& geo)
+{
+    std::fill(m_a, m_a + geo.get_range(), val);
+}
+
+template<class P, typename T>
+inline void Grid<P, T>::fill_all(const T& val)
+{
+    std::fill(m_a, m_a + Point::range_onboard, val);
+}
+
+template<class P, typename T>
+void Grid<P, T>::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<T>::value, "");
+#endif
+    memcpy(&m_a, grid.m_a, geo.get_range() * sizeof(T));
+}
+
+template<class P, typename T>
+string Grid<P, T>::to_string(const Geometry& geo) const
+{
+    return grid_to_string(*this, geo);
+}
+
+//-----------------------------------------------------------------------------
+
+/** Like Grid, but allows Point::null() as index. */
+template<class P, typename T>
+class GridExt
+{
+public:
+    typedef P Point;
+
+    typedef libboardgame_base::Geometry<P> 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<P, T>& grid, const Geometry& geo);
+
+    void copy_from(const GridExt& grid, const Geometry& geo);
+
+private:
+    T m_a[Point::range];
+};
+
+template<class P, typename T>
+inline T& GridExt<P, T>::operator[](const Point& p)
+{
+    return m_a[p.to_int()];
+}
+
+template<class P, typename T>
+inline const T& GridExt<P, T>::operator[](const Point& p) const
+{
+    return m_a[p.to_int()];
+}
+
+template<class P, typename T>
+inline void GridExt<P, T>::fill(const T& val, const Geometry& geo)
+{
+    std::fill(m_a, m_a + geo.get_range(), val);
+}
+
+template<class P, typename T>
+inline void GridExt<P, T>::fill_all(const T& val)
+{
+    std::fill(m_a, m_a + Point::range, val);
+}
+
+template<class P, typename T>
+inline void GridExt<P, T>::copy_from(const Grid<P, T>& grid,
+                                     const Geometry& geo)
+{
+    copy(grid.m_a, grid.m_a + geo.get_range(), m_a);
+}
+
+template<class P, typename T>
+inline void GridExt<P, T>::copy_from(const GridExt& grid,
+                                          const Geometry& geo)
+{
+    copy(grid.m_a, grid.m_a + geo.get_range(), m_a);
+}
+
+template<class P, typename T>
+string GridExt<P, T>::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 (file)
index 0000000..591fb0a
--- /dev/null
@@ -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 <algorithm>
+#include <limits>
+
+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 P>
+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<unsigned>::max() times. */
+    void setup_for_overflow_test(unsigned nu_clear);
+
+private:
+    unsigned m_current;
+
+    unsigned m_a[Point::range];
+
+    void reset();
+};
+
+template<class P>
+inline Marker<P>::Marker()
+{
+    reset();
+}
+
+template<class P>
+bool Marker<P>::operator[](Point p) const
+{
+    return m_a[p.to_int()] == m_current;
+}
+
+template<class P>
+inline void Marker<P>::clear()
+{
+    if (--m_current == 0)
+        reset();
+}
+
+template<class P>
+inline void Marker<P>::setup_for_overflow_test(unsigned nu_clear)
+{
+    reset();
+    m_current -= nu_clear;
+}
+
+template<class P>
+inline void Marker<P>::reset()
+{
+    m_current = numeric_limits<unsigned>::max() - 1;
+    fill(m_a, m_a + Point::range, numeric_limits<unsigned>::max());
+}
+
+template<class P>
+inline bool Marker<P>::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 (file)
index 0000000..f0e0a5e
--- /dev/null
@@ -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 <limits>
+#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<unsigned M, unsigned W, unsigned H, typename I>
+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<I>::is_integer, "");
+
+    static_assert(! numeric_limits<I>::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<unsigned M, unsigned W, unsigned H, typename I>
+inline Point<M, W, H, I>::Point()
+{
+#if LIBBOARDGAME_DEBUG
+    m_i = value_uninitialized;
+#endif
+}
+
+template<unsigned M, unsigned W, unsigned H, typename I>
+inline Point<M, W, H, I>::Point(unsigned i)
+{
+    LIBBOARDGAME_ASSERT(i < range);
+    m_i = static_cast<I>(i);
+}
+
+template<unsigned M, unsigned W, unsigned H, typename I>
+inline bool Point<M, W, H, I>::operator==(const Point& p) const
+{
+    LIBBOARDGAME_ASSERT(is_initialized());
+    LIBBOARDGAME_ASSERT(p.is_initialized());
+    return m_i == p.m_i;
+}
+
+template<unsigned M, unsigned W, unsigned H, typename I>
+inline bool Point<M, W, H, I>::operator!=(const Point& p) const
+{
+    return ! operator==(p);
+}
+
+template<unsigned M, unsigned W, unsigned H, typename I>
+inline bool Point<M, W, H, I>::operator<(const Point& p) const
+{
+    LIBBOARDGAME_ASSERT(is_initialized());
+    LIBBOARDGAME_ASSERT(p.is_initialized());
+    return m_i < p.m_i;
+}
+
+template<unsigned M, unsigned W, unsigned H, typename I>
+inline bool Point<M, W, H, I>::is_initialized() const
+{
+    return m_i < value_uninitialized;
+}
+
+template<unsigned M, unsigned W, unsigned H, typename I>
+inline bool Point<M, W, H, I>::is_null() const
+{
+    LIBBOARDGAME_ASSERT(is_initialized());
+    return m_i == value_null;
+}
+
+template<unsigned M, unsigned W, unsigned H, typename I>
+inline auto Point<M, W, H, I>::null() -> Point
+{
+    return Point(value_null);
+}
+
+template<unsigned M, unsigned W, unsigned H, typename I>
+inline unsigned Point<M, W, H, I>::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 (file)
index 0000000..e86545c
--- /dev/null
@@ -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 <cmath>
+#include "Geometry.h"
+#include "libboardgame_util/Unused.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+/** %Transform a point.
+    @tparam P An instance of class Point. */
+template<class P>
+class PointTransform
+{
+public:
+    typedef P Point;
+
+    virtual ~PointTransform();
+
+    virtual Point get_transformed(const Point& p,
+                                  const Geometry<P>& geo) const = 0;
+};
+
+template<class P>
+PointTransform<P>::~PointTransform() = default;
+
+//-----------------------------------------------------------------------------
+
+template<class P>
+class PointTransfIdent
+    : public PointTransform<P>
+{
+public:
+    typedef P Point;
+
+    Point get_transformed(const Point& p,
+                          const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfIdent<P>::get_transformed(const Point& p,
+                                       const Geometry<P>& geo) const
+{
+    LIBBOARDGAME_UNUSED(geo);
+    return p;
+}
+
+//-----------------------------------------------------------------------------
+
+/** Rotate point by 90 degrees. */
+template<class P>
+class PointTransfRot90
+    : public PointTransform<P>
+{
+public:
+    typedef P Point;
+
+    Point get_transformed(const Point& p,
+                          const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfRot90<P>::get_transformed(const Point& p,
+                                       const Geometry<P>& 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 P>
+class PointTransfRot180
+    : public PointTransform<P>
+{
+public:
+    typedef P Point;
+
+    Point get_transformed(const Point& p,
+                          const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfRot180<P>::get_transformed(const Point& p,
+                                        const Geometry<P>& 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 P>
+class PointTransfRot270
+    : public PointTransform<P>
+{
+public:
+    typedef P Point;
+
+    Point get_transformed(const Point& p,
+                          const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfRot270<P>::get_transformed(const Point& p,
+                                        const Geometry<P>& 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 P>
+class PointTransfRot270Refl
+    : public PointTransform<P>
+{
+public:
+    typedef P Point;
+
+    Point get_transformed(const Point& p,
+                          const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfRot270Refl<P>::get_transformed(const Point& p,
+                                            const Geometry<P>& 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 P>
+class PointTransfRot90Refl
+    : public PointTransform<P>
+{
+public:
+    typedef P Point;
+
+    Point get_transformed(const Point& p,
+                          const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfRot90Refl<P>::get_transformed(const Point& p,
+                                           const Geometry<P>& 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 P>
+class PointTransfRefl
+    : public PointTransform<P>
+{
+public:
+    typedef P Point;
+
+    Point get_transformed(const Point& p,
+                          const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfRefl<P>::get_transformed(const Point& p,
+                                      const Geometry<P>& 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 P>
+class PointTransfReflRot180
+    : public PointTransform<P>
+{
+public:
+    typedef P Point;
+
+    Point get_transformed(const Point& p,
+                          const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfReflRot180<P>::get_transformed(const Point& p,
+                                            const Geometry<P>& 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 P>
+class PointTransfTrigonRot60
+    : public PointTransform<P>
+{
+public:
+    typedef P Point;
+
+    Point get_transformed(const Point& p,
+                          const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfTrigonRot60<P>::get_transformed(const Point& p,
+                                             const Geometry<P>& geo) const
+{
+    float cx = 0.5f * static_cast<float>(geo.get_width() - 1);
+    float cy = 0.5f * static_cast<float>(geo.get_height() - 1);
+    float px = static_cast<float>(geo.get_x(p)) - cx;
+    float py = static_cast<float>(geo.get_y(p)) - cy;
+    unsigned x = static_cast<unsigned>(round(cx + 0.5f * px + 1.5f * py));
+    unsigned y = static_cast<unsigned>(round(cy - 0.5f * px + 0.5f * py));
+    return geo.get_point(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+template<class P>
+class PointTransfTrigonRot120
+    : public PointTransform<P>
+{
+public:
+    typedef P Point;
+
+    Point get_transformed(const Point& p,
+                          const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfTrigonRot120<P>::get_transformed(const Point& p,
+                                              const Geometry<P>& geo) const
+{
+    float cx = 0.5f * static_cast<float>(geo.get_width() - 1);
+    float cy = 0.5f * static_cast<float>(geo.get_height() - 1);
+    float px = static_cast<float>(geo.get_x(p)) - cx;
+    float py = static_cast<float>(geo.get_y(p)) - cy;
+    unsigned x = static_cast<unsigned>(round(cx - 0.5f * px + 1.5f * py));
+    unsigned y = static_cast<unsigned>(round(cy - 0.5f * px - 0.5f * py));
+    return geo.get_point(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+template<class P>
+class PointTransfTrigonRot240
+    : public PointTransform<P>
+{
+public:
+    typedef P Point;
+
+    Point get_transformed(const Point& p,
+                          const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfTrigonRot240<P>::get_transformed(const Point& p,
+                                              const Geometry<P>& geo) const
+{
+    float cx = 0.5f * static_cast<float>(geo.get_width() - 1);
+    float cy = 0.5f * static_cast<float>(geo.get_height() - 1);
+    float px = static_cast<float>(geo.get_x(p)) - cx;
+    float py = static_cast<float>(geo.get_y(p)) - cy;
+    unsigned x = static_cast<unsigned>(round(cx - 0.5f * px - 1.5f * py));
+    unsigned y = static_cast<unsigned>(round(cy + 0.5f * px - 0.5f * py));
+    return geo.get_point(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+template<class P>
+class PointTransfTrigonRot300
+    : public PointTransform<P>
+{
+public:
+    typedef P Point;
+
+    Point get_transformed(const Point& p,
+                          const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfTrigonRot300<P>::get_transformed(const Point& p,
+                                              const Geometry<P>& geo) const
+{
+    float cx = 0.5f * static_cast<float>(geo.get_width() - 1);
+    float cy = 0.5f * static_cast<float>(geo.get_height() - 1);
+    float px = static_cast<float>(geo.get_x(p)) - cx;
+    float py = static_cast<float>(geo.get_y(p)) - cy;
+    unsigned x = static_cast<unsigned>(round(cx + 0.5f * px - 1.5f * py));
+    unsigned y = static_cast<unsigned>(round(cy + 0.5f * px + 0.5f * py));
+    return geo.get_point(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+template<class P>
+class PointTransfTrigonReflRot60
+    : public PointTransform<P>
+{
+public:
+    typedef P Point;
+
+    Point get_transformed(const Point& p,
+                          const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfTrigonReflRot60<P>::get_transformed(const Point& p,
+                                                 const Geometry<P>& geo) const
+{
+    float cx = 0.5f * static_cast<float>(geo.get_width() - 1);
+    float cy = 0.5f * static_cast<float>(geo.get_height() - 1);
+    float px = static_cast<float>(geo.get_x(p)) - cx;
+    float py = static_cast<float>(geo.get_y(p)) - cy;
+    unsigned x = static_cast<unsigned>(round(cx + 0.5f * (-px) + 1.5f * py));
+    unsigned y = static_cast<unsigned>(round(cy - 0.5f * (-px) + 0.5f * py));
+    return geo.get_point(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+template<class P>
+class PointTransfTrigonReflRot120
+    : public PointTransform<P>
+{
+public:
+    typedef P Point;
+
+    Point get_transformed(const Point& p,
+                          const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfTrigonReflRot120<P>::get_transformed(const Point& p,
+                                                  const Geometry<P>& geo) const
+{
+    float cx = 0.5f * static_cast<float>(geo.get_width() - 1);
+    float cy = 0.5f * static_cast<float>(geo.get_height() - 1);
+    float px = static_cast<float>(geo.get_x(p)) - cx;
+    float py = static_cast<float>(geo.get_y(p)) - cy;
+    unsigned x = static_cast<unsigned>(round(cx - 0.5f * (-px) + 1.5f * py));
+    unsigned y = static_cast<unsigned>(round(cy - 0.5f * (-px) - 0.5f * py));
+    return geo.get_point(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+template<class P>
+class PointTransfTrigonReflRot240
+    : public PointTransform<P>
+{
+public:
+    typedef P Point;
+
+    Point get_transformed(const Point& p,
+                          const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfTrigonReflRot240<P>::get_transformed(const Point& p,
+                                                  const Geometry<P>& geo) const
+{
+    float cx = 0.5f * static_cast<float>(geo.get_width() - 1);
+    float cy = 0.5f * static_cast<float>(geo.get_height() - 1);
+    float px = static_cast<float>(geo.get_x(p)) - cx;
+    float py = static_cast<float>(geo.get_y(p)) - cy;
+    unsigned x = static_cast<unsigned>(round(cx - 0.5f * (-px) - 1.5f * py));
+    unsigned y = static_cast<unsigned>(round(cy + 0.5f * (-px) - 0.5f * py));
+    return geo.get_point(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+template<class P>
+class PointTransfTrigonReflRot300
+    : public PointTransform<P>
+{
+public:
+    typedef P Point;
+
+    Point get_transformed(const Point& p,
+                          const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfTrigonReflRot300<P>::get_transformed(const Point& p,
+                                                  const Geometry<P>& geo) const
+{
+    float cx = 0.5f * static_cast<float>(geo.get_width() - 1);
+    float cy = 0.5f * static_cast<float>(geo.get_height() - 1);
+    float px = static_cast<float>(geo.get_x(p)) - cx;
+    float py = static_cast<float>(geo.get_y(p)) - cy;
+    unsigned x = static_cast<unsigned>(round(cx + 0.5f * (-px) - 1.5f * py));
+    unsigned y = static_cast<unsigned>(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 (file)
index 0000000..9fd9136
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "Rating.h"
+
+#include <cmath>
+#include <iostream>
+#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<float>(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 (file)
index 0000000..8b190a7
--- /dev/null
@@ -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 <cmath>
+#include <iosfwd>
+
+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<int>(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 (file)
index 0000000..ea47e0a
--- /dev/null
@@ -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 <map>
+#include <memory>
+#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 P>
+class RectGeometry final
+    : public Geometry<P>
+{
+public:
+    typedef P Point;
+
+    using AdjCoordList = typename Geometry<P>::AdjCoordList;
+    using DiagCoordList = typename Geometry<P>::DiagCoordList;
+    using AdjList = typename Geometry<P>::AdjList;
+    using DiagList = typename Geometry<P>::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<pair<unsigned, unsigned>, shared_ptr<RectGeometry>> s_geometry;
+};
+
+template<class P>
+map<pair<unsigned, unsigned>, shared_ptr<RectGeometry<P>>>
+    RectGeometry<P>::s_geometry;
+
+template<class P>
+RectGeometry<P>::RectGeometry(unsigned width, unsigned height)
+{
+    Geometry<P>::init(width, height);
+}
+
+template<class P>
+const RectGeometry<P>& RectGeometry<P>::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<RectGeometry>(width, height);
+    return *s_geometry.insert(make_pair(key, geometry)).first->second;
+}
+
+template<class P>
+auto RectGeometry<P>::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<class P>
+auto RectGeometry<P>::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<class P>
+unsigned RectGeometry<P>::get_period_x() const
+{
+    return 1;
+}
+
+template<class P>
+unsigned RectGeometry<P>::get_period_y() const
+{
+    return 1;
+}
+
+template<class P>
+unsigned RectGeometry<P>::get_point_type(int x, int y) const
+{
+    LIBBOARDGAME_UNUSED(x);
+    LIBBOARDGAME_UNUSED(y);
+    return 0;
+}
+
+template<class P>
+bool RectGeometry<P>::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 (file)
index 0000000..5553da4
--- /dev/null
@@ -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 <config.h>
+#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 (file)
index 0000000..823de62
--- /dev/null
@@ -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 (file)
index 0000000..4063939
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "StringRep.h"
+
+#include <cstdio>
+#include <iostream>
+#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 (file)
index 0000000..db6a674
--- /dev/null
@@ -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 <iosfwd>
+
+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 (file)
index 0000000..b07cf76
--- /dev/null
@@ -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 <config.h>
+#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 (file)
index 0000000..8e170dc
--- /dev/null
@@ -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<class I>
+    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<class I>
+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 (file)
index 0000000..019a619
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "Arguments.h"
+
+#include <cctype>
+
+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<char>(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 (file)
index 0000000..8a4bc4a
--- /dev/null
@@ -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 <cxxabi.h>
+#endif
+#include <sstream>
+#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<typename T>
+    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<typename T>
+    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<typename T>
+    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<typename T>
+    T parse_min_max(unsigned i, T min, T max) const;
+
+    template<typename T>
+    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<typename T>
+    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<unsigned>(m_line.get_elements().size())
+        - m_line.get_idx_name() - 1;
+}
+
+template<typename T>
+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<typename T>
+T Arguments::parse() const
+{
+    check_size(1);
+    return parse<T>(0);
+}
+
+template<typename T>
+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<T>() << ")";
+        throw Failure(msg.str());
+    }
+    return result;
+}
+
+template<typename T>
+T Arguments::parse_min(unsigned i, T min) const
+{
+    T result = parse<T>(i);
+    if (result < min)
+    {
+        ostringstream msg;
+        msg << "argument " << (i + 1) << " must be greater or equal " << min;
+        throw Failure(msg.str());
+    }
+    return result;
+}
+
+template<typename T>
+T Arguments::parse_min_max(T min, T max) const
+{
+    check_size(1);
+    return parse_min_max<T>(0, min, max);
+}
+
+template<typename T>
+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 (file)
index 0000000..fea26bd
--- /dev/null
@@ -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 (file)
index 0000000..7fe7f5f
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "CmdLine.h"
+
+#include <cassert>
+#include <limits>
+#include <sstream>
+
+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<unsigned>::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<unsigned char>(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<unsigned char>(*begin)))
+        ++begin;
+    auto end = m_line.end();
+    while (end > begin && isspace(static_cast<unsigned char>(*(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 (file)
index 0000000..c51b966
--- /dev/null
@@ -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 <algorithm>
+#include <cassert>
+#include <string>
+#include <iterator>
+#include <vector>
+#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<CmdLineRange>& 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<CmdLineRange> 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<CmdLineRange>& 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<char>(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 (file)
index 0000000..24ef9ba
--- /dev/null
@@ -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 <iosfwd>
+#include <algorithm>
+#include <string>
+
+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 (file)
index 0000000..f0e4e04
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "Engine.h"
+
+#include <cctype>
+#include <iostream>
+#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<unsigned char>(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 (file)
index 0000000..b043164
--- /dev/null
@@ -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 <functional>
+#include <iosfwd>
+#include <map>
+#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<void(const Arguments&, Response&)> Handler;
+
+    typedef function<void(Response&)> HandlerNoArgs;
+
+    typedef function<void(const Arguments&)> HandlerNoResponse;
+
+    typedef function<void()> HandlerNoArgsNoResponse;
+
+    /** @page libboardgame_gtp_commands libboardgame_gtp::Engine GTP commands
+        <dl>
+        <dt>@link cmd_known_command() @c known_command @endlink</dt>
+        <dd>@copydoc cmd_known_command() </dd>
+        <dt>@link cmd_list_commands() @c list_commands @endlink</dt>
+        <dd>@copydoc cmd_list_commands() </dd>
+        <dt>@link cmd_name() @c name @endlink</dt>
+        <dd>@copydoc cmd_name() </dd>
+        <dt>@link cmd_protocol_version() @c protocol_version @endlink</dt>
+        <dd>@copydoc cmd_protocol_version() </dd>
+        <dt>@link cmd_quit() @c quit @endlink</dt>
+        <dd>@copydoc cmd_quit() </dd>
+        <dt>@link cmd_version() @c version @endlink</dt>
+        <dd>@copydoc cmd_version() </dd>
+        </dl> */
+    /** @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<class T>
+    void add(const string& name,
+             void (T::*f)(const Arguments&, Response&), T* t);
+
+    template<class T>
+    void add(const string& name, void (T::*f)(const Arguments&), T* t);
+
+    template<class T>
+    void add(const string& name, void (T::*f)(Response&), T* t);
+
+    template<class T>
+    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<class T>
+    void add(const string& name, void (T::*f)(const Arguments&, Response&));
+
+    template<class T>
+    void add(const string& name, void (T::*f)(const Arguments&));
+
+    template<class T>
+    void add(const string& name, void (T::*f)(Response&));
+
+    template<class T>
+    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<string, Handler> 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<class T>
+void Engine::add(const string& name, void (T::*f)(const Arguments&, Response&))
+{
+    add(name, f, dynamic_cast<T*>(this));
+}
+
+template<class T>
+void Engine::add(const string& name, void (T::*f)(Response&))
+{
+    add(name, f, dynamic_cast<T*>(this));
+}
+
+template<class T>
+void Engine::add(const string& name, void (T::*f)(const Arguments&))
+{
+    add(name, f, dynamic_cast<T*>(this));
+}
+
+template<class T>
+void Engine::add(const string& name, void (T::*f)())
+{
+    add(name, f, dynamic_cast<T*>(this));
+}
+
+template<class T>
+void Engine::add(const string& name,
+                 void (T::*f)(const Arguments&, Response&), T* t)
+{
+    assert(f);
+    add(name,
+        static_cast<Handler>(bind(f, t, placeholders::_1, placeholders::_2)));
+}
+
+template<class T>
+void Engine::add(const string& name, void (T::*f)(Response&), T* t)
+{
+    assert(f);
+    add(name, static_cast<HandlerNoArgs>(bind(f, t, placeholders::_1)));
+}
+
+template<class T>
+void Engine::add(const string& name, void (T::*f)(const Arguments&), T* t)
+{
+    assert(f);
+    add(name, static_cast<HandlerNoResponse>(bind(f, t, placeholders::_1)));
+}
+
+template<class T>
+void Engine::add(const string& name, void (T::*f)(), T* t)
+{
+    assert(f);
+    add(name, static_cast<HandlerNoArgsNoResponse>(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 (file)
index 0000000..90eee8a
--- /dev/null
@@ -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 <stdexcept>
+
+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 (file)
index 0000000..5868b3e
--- /dev/null
@@ -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 <config.h>
+#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 (file)
index 0000000..2805818
--- /dev/null
@@ -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 <sstream>
+#include <string>
+
+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<typename TYPE>
+inline Response& operator<<(Response& r, const TYPE& t)
+{
+    static_cast<ostream&>(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 (file)
index 0000000..bf6bdb8
--- /dev/null
@@ -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 <atomic>
+#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<typename T, bool MT> struct Atomic;
+
+template<typename T>
+struct Atomic<T, false>
+{
+    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<typename T>
+struct Atomic<T, true>
+{
+    atomic<T> 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 (file)
index 0000000..e0b4461
--- /dev/null
@@ -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 (file)
index 0000000..25e4485
--- /dev/null
@@ -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 <algorithm>
+#include <cstring>
+#include <memory>
+#include <random>
+#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 M, unsigned P, size_t S, bool MT>
+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<typename Move::IntType, MT> m_lgr1[max_players][Move::range];
+
+    Atomic<typename Move::IntType, MT> m_lgr2[max_players][hash_table_size];
+
+    size_t get_index(Move last, Move second_last) const;
+};
+
+template<class M, unsigned P, size_t S, bool MT>
+LastGoodReply<M, P, S, MT>::LastGoodReply()
+{
+    mt19937 generator;
+    for (auto& hash : m_hash)
+        hash = generator();
+}
+
+template<class M, unsigned P, size_t S, bool MT>
+inline size_t LastGoodReply<M, P, S, MT>::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<class M, unsigned P, size_t S, bool MT>
+inline auto LastGoodReply<M, P, S, MT>::get_lgr1(PlayerInt player,
+                                                 Move last) const -> Move
+{
+    return Move(m_lgr1[player][last.to_int()].load(memory_order_relaxed));
+}
+
+template<class M, unsigned P, size_t S, bool MT>
+inline auto LastGoodReply<M, P, S, MT>::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<class M, unsigned P, size_t S, bool MT>
+void LastGoodReply<M, P, S, MT>::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<class M, unsigned P, size_t S, bool MT>
+inline void LastGoodReply<M, P, S, MT>::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<class M, unsigned P, size_t S, bool MT>
+inline void LastGoodReply<M, P, S, MT>::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 (file)
index 0000000..5bb8bdd
--- /dev/null
@@ -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 <cstdint>
+#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<typename M, typename F, bool MT>
+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<Float, MT> m_value;
+
+    Atomic<Float, MT> m_value_count;
+
+    Atomic<Float, MT> m_visit_count;
+
+    Atomic<unsigned short, MT> m_nu_children;
+
+    Move m_move;
+
+    NodeIdx m_first_child;
+};
+
+template<typename M, typename F, bool MT>
+void Node<M, F, MT>::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<typename M, typename F, bool MT>
+void Node<M, F, MT>::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<typename M, typename F, bool MT>
+void Node<M, F, MT>::copy_data_from(const Node& node)
+{
+    // Reminder to update this function when the class gets additional members
+    struct Dummy
+    {
+        Atomic<Float, MT> m_value;
+        Atomic<Float, MT> m_value_count;
+        Atomic<Float, MT> m_visit_count;
+        Atomic<unsigned short, MT> 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<typename M, typename F, bool MT>
+inline auto Node<M, F, MT>::get_value_count() const -> Float
+{
+    return m_value_count.load(memory_order_relaxed);
+}
+
+template<typename M, typename F, bool MT>
+inline NodeIdx Node<M, F, MT>::get_first_child() const
+{
+    LIBBOARDGAME_ASSERT(has_children());
+    return m_first_child;
+}
+
+template<typename M, typename F, bool MT>
+inline auto Node<M, F, MT>::get_move() const -> const Move&
+{
+    return m_move;
+}
+
+template<typename M, typename F, bool MT>
+inline unsigned short Node<M, F, MT>::get_nu_children() const
+{
+    return m_nu_children.load(memory_order_acquire);
+}
+
+template<typename M, typename F, bool MT>
+inline auto Node<M, F, MT>::get_value() const -> Float
+{
+    return m_value.load(memory_order_relaxed);
+}
+
+template<typename M, typename F, bool MT>
+inline auto Node<M, F, MT>::get_visit_count() const -> Float
+{
+    return m_visit_count.load(memory_order_relaxed);
+}
+
+template<typename M, typename F, bool MT>
+inline bool Node<M, F, MT>::has_children() const
+{
+    return get_nu_children() > 0;
+}
+
+template<typename M, typename F, bool MT>
+inline void Node<M, F, MT>::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<typename M, typename F, bool MT>
+void Node<M, F, MT>::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<typename M, typename F, bool MT>
+void Node<M, F, MT>::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<typename M, typename F, bool MT>
+inline void Node<M, F, MT>::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<typename M, typename F, bool MT>
+inline void Node<M, F, MT>::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<typename M, typename F, bool MT>
+inline void Node<M, F, MT>::unlink_children()
+{
+    m_nu_children.store(0, memory_order_release);
+}
+
+template<typename M, typename F, bool MT>
+inline void Node<M, F, MT>::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 (file)
index 0000000..1bf6b9a
--- /dev/null
@@ -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 <cstdint>
+
+namespace libboardgame_mcts {
+
+//-----------------------------------------------------------------------------
+
+typedef uint_fast8_t PlayerInt;
+
+//-----------------------------------------------------------------------------
+
+template<typename MOVE>
+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 (file)
index 0000000..1a99927
--- /dev/null
@@ -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 <array>
+#include <condition_variable>
+#include <functional>
+#include <mutex>
+#include <thread>
+#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 S, class M, class R = SearchParamConstDefault>
+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<M, Float, multithread> Node;
+
+    typedef libboardgame_mcts::Tree<Node> Tree;
+
+    typedef libboardgame_mcts::PlayerMove<M> 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<State> 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<Move, max_moves>& 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<void(double, double)> callback);
+
+    /** Get evaluation for a player at root node. */
+    const StatisticsDirtyLockFree<Float>& get_root_val(PlayerInt player) const;
+
+    /** Get evaluation for get_player() at root node. */
+    const StatisticsDirtyLockFree<Float>& 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<const Node*, max_moves> nodes;
+
+        ArrayList<PlayerMove, max_moves> moves;
+
+        array<Float, max_players> 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> 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<PlayerInt, Move::range> was_played;
+
+        /** Local variable for update_rave().
+            Reused for efficiency. */
+        array<unsigned, Move::range> 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<void(ThreadState&)> 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<mutex> 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<StatisticsDirtyLockFree<Float>, max_players> m_root_val;
+
+    LastGoodReply<Move, max_players, lgr_hash_table_size, multithread> m_lgr;
+
+    /** See get_nu_simulations(). */
+    Atomic<size_t, multithread> 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<unique_ptr<Thread>> m_threads;
+
+    Tree m_tmp_tree;
+
+#if LIBBOARDGAME_DEBUG
+    AssertionHandler m_assertion_handler;
+#endif
+
+
+    function<void(double, double)> m_callback;
+
+    ArrayList<Move, max_moves> 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<class S, class M, class R>
+SearchBase<S, M, R>::ThreadState::~ThreadState() = default;
+
+template<class S, class M, class R>
+SearchBase<S, M, R>::Thread::Thread(SearchFunc& search_func)
+    : m_search_func(search_func)
+{ }
+
+template<class S, class M, class R>
+SearchBase<S, M, R>::Thread::~Thread()
+{
+    if (! m_thread.joinable())
+        return;
+    m_quit = true;
+    {
+        lock_guard<mutex> lock(m_start_search_mutex);
+        m_start_search_flag = true;
+    }
+    m_start_search_cond.notify_one();
+    m_thread.join();
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::Thread::run()
+{
+    m_thread = thread(bind(&Thread::thread_main, this));
+    m_thread_ready.wait();
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::Thread::start_search()
+{
+    LIBBOARDGAME_ASSERT(m_thread.joinable());
+    m_search_finished_lock.lock();
+    {
+        lock_guard<mutex> lock(m_start_search_mutex);
+        m_start_search_flag = true;
+    }
+    m_start_search_cond.notify_one();
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::Thread::thread_main()
+{
+    unique_lock<mutex> 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<mutex> lock(m_search_finished_mutex);
+            m_search_finished_flag = true;
+        }
+        m_search_finished_cond.notify_one();
+    }
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::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<class S, class M, class R>
+SearchBase<S, M, R>::AssertionHandler::AssertionHandler(
+        const SearchBase& search)
+    : m_search(search)
+{
+}
+
+template<class S, class M, class R>
+SearchBase<S, M, R>::AssertionHandler::~AssertionHandler() = default;
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::AssertionHandler::run()
+{
+    LIBBOARDGAME_LOG(m_search.dump());
+}
+#endif // LIBBOARDGAME_DEBUG
+
+
+template<class S, class M, class R>
+SearchBase<S, M, R>::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<class S, class M, class R>
+SearchBase<S, M, R>::~SearchBase() = default;
+
+template<class S, class M, class R>
+bool SearchBase<S, M, R>::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<class S, class M, class R>
+bool SearchBase<S, M, R>::check_abort_expensive(
+        ThreadState& thread_state) const
+{
+    if (get_abort())
+    {
+        LIBBOARDGAME_LOG_THREAD(thread_state, "Search aborted");
+        return true;
+    }
+    static_assert(numeric_limits<Float>::radix == 2, "");
+    auto count = m_tree.get_root().get_visit_count();
+    if (count >= (size_t(1) << numeric_limits<Float>::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<class S, class M, class R>
+bool SearchBase<S, M, R>::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<class S, class M, class R>
+bool SearchBase<S, M, R>::check_followup(ArrayList<Move, max_moves>& sequence)
+{
+    LIBBOARDGAME_UNUSED(sequence);
+    return false;
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::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<typename Thread::SearchFunc>(
+                          bind(&SearchBase::search_loop, this, placeholders::_1));
+    for (unsigned i = 0; i < m_nu_threads; ++i)
+    {
+        unique_ptr<Thread> 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<class S, class M, class R>
+string SearchBase<S, M, R>::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<class S, class M, class R>
+bool SearchBase<S, M, R>::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<class S, class M, class R>
+inline auto SearchBase<S, M, R>::get_expand_threshold() const -> Float
+{
+    return m_expand_threshold;
+}
+
+template<class S, class M, class R>
+inline auto SearchBase<S, M, R>::get_expand_threshold_inc() const -> Float
+{
+    return m_expand_threshold_inc;
+}
+
+template<class S, class M, class R>
+inline size_t SearchBase<S, M, R>::get_nu_simulations() const
+{
+    return m_nu_simulations;
+}
+
+template<class S, class M, class R>
+inline auto SearchBase<S, M, R>::get_root_val(PlayerInt player) const
+-> const StatisticsDirtyLockFree<Float>&
+{
+    LIBBOARDGAME_ASSERT(player < m_nu_players);
+    return m_root_val[player];
+}
+
+template<class S, class M, class R>
+inline auto SearchBase<S, M, R>::get_root_val() const
+-> const StatisticsDirtyLockFree<Float>&
+{
+    return get_root_val(get_player());
+}
+
+template<class S, class M, class R>
+inline auto SearchBase<S, M, R>::get_root_visit_count() const -> Float
+{
+    return m_tree.get_root().get_visit_count();
+}
+
+template<class S, class M, class R>
+inline auto SearchBase<S, M, R>::get_rave_parent_max() const -> Float
+{
+    return m_rave_parent_max;
+}
+
+template<class S, class M, class R>
+inline auto SearchBase<S, M, R>::get_rave_child_max() const -> Float
+{
+    return m_rave_child_max;
+}
+
+template<class S, class M, class R>
+inline auto SearchBase<S, M, R>::get_rave_weight() const -> Float
+{
+    return m_rave_weight;
+}
+
+template<class S, class M, class R>
+inline bool SearchBase<S, M, R>::get_reuse_subtree() const
+{
+    return m_reuse_subtree;
+}
+
+template<class S, class M, class R>
+inline bool SearchBase<S, M, R>::get_reuse_tree() const
+{
+    return m_reuse_tree;
+}
+
+template<class S, class M, class R>
+inline S& SearchBase<S, M, R>::get_state(unsigned thread_id)
+{
+    LIBBOARDGAME_ASSERT(thread_id < m_threads.size());
+    return *m_threads[thread_id]->thread_state.state;
+}
+
+template<class S, class M, class R>
+inline const S& SearchBase<S, M, R>::get_state(unsigned thread_id) const
+{
+    LIBBOARDGAME_ASSERT(thread_id < m_threads.size());
+    return *m_threads[thread_id]->thread_state.state;
+}
+
+template<class S, class M, class R>
+inline TimeSource& SearchBase<S, M, R>::get_time_source()
+{
+    LIBBOARDGAME_ASSERT(m_time_source != 0);
+    return *m_time_source;
+}
+
+template<class S, class M, class R>
+inline auto SearchBase<S, M, R>::get_tree() const -> const Tree&
+{
+    return m_tree;
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::on_start_search(bool is_followup)
+{
+    // Default implementation does nothing
+    LIBBOARDGAME_UNUSED(is_followup);
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::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<class S, class M, class R>
+void SearchBase<S, M, R>::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<class S, class M, class R>
+string SearchBase<S, M, R>::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<class S, class M, class R>
+string SearchBase<S, M, R>::get_info_ext() const
+{
+    return string();
+}
+
+template<class S, class M, class R>
+bool SearchBase<S, M, R>::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<Float>::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<class S, class M, class R>
+bool SearchBase<S, M, R>::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<class S, class M, class R>
+bool SearchBase<S, M, R>::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<double>::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<class S, class M, class R>
+void SearchBase<S, M, R>::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<unsigned>(
+                    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<class S, class M, class R>
+inline auto SearchBase<S, M, R>::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<class S, class M, class R>
+auto SearchBase<S, M, R>::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<class S, class M, class R>
+bool SearchBase<S, M, R>::select_move(Move& mv) const
+{
+    auto child = select_final();
+    if (child)
+    {
+        mv = child->get_move();
+        return true;
+    }
+    else
+        return false;
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::set_callback(function<void(double, double)> callback)
+{
+    m_callback = callback;
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::set_expand_threshold(Float n)
+{
+    m_expand_threshold = n;
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::set_expand_threshold_inc(Float n)
+{
+    m_expand_threshold_inc = n;
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::set_rave_parent_max(Float n)
+{
+    m_rave_parent_max = n;
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::set_rave_child_max(Float n)
+{
+    m_rave_child_max = n;
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::set_rave_weight(Float v)
+{
+    m_rave_weight = v;
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::set_reuse_subtree(bool enable)
+{
+    m_reuse_subtree = enable;
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::set_reuse_tree(bool enable)
+{
+    m_reuse_tree = enable;
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::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<bool,max_players> 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<class S, class M, class R>
+void SearchBase<S, M, R>::update_rave(ThreadState& thread_state)
+{
+    const auto& state = *thread_state.state;
+    auto& moves = thread_state.simulation.moves;
+    auto nu_moves = static_cast<unsigned>(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<unsigned>(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<Float>(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<Float>(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<class S, class M, class R>
+void SearchBase<S, M, R>::update_values(ThreadState& thread_state)
+{
+    const auto& simulation = thread_state.simulation;
+    auto& nodes = simulation.nodes;
+    auto& eval = simulation.eval;
+    unsigned nu_nodes = static_cast<unsigned>(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 (file)
index 0000000..ba5f0ed
--- /dev/null
@@ -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 <algorithm>
+#include <memory>
+#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.<p>
+    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<typename N>
+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<Float>::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<Node[]> m_nodes;
+
+    unique_ptr<ThreadStorage[]> 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<typename N>
+inline Tree<N>::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<typename N>
+inline void Tree<N>::NodeExpander::add_child(const Move& mv, Float value,
+                                             Float count)
+{
+    // -numeric_limits<Float>::max() ist init value for m_best_value
+    LIBBOARDGAME_ASSERT(value > -numeric_limits<Float>::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<typename N>
+inline bool Tree<N>::NodeExpander::check_capacity(
+        unsigned short nu_children) const
+{
+    return m_thread_storage.end - m_thread_storage.next  >= nu_children;
+}
+
+template<typename N>
+inline auto Tree<N>::NodeExpander::get_best_child() const -> const Node*
+{
+    return m_best_child;
+}
+
+template<typename N>
+inline auto Tree<N>::get_node(NodeIdx i) const -> const Node&
+{
+    return m_nodes[i];
+}
+
+template<typename N>
+inline void Tree<N>::NodeExpander::link_children(Tree& tree, const Node& node)
+{
+    auto nu_children =
+            static_cast<unsigned short>(m_thread_storage.next - m_first_child);
+    tree.link_children(node, m_first_child, nu_children);
+}
+
+
+template<typename N>
+Tree<N>::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<size_t>(numeric_limits<NodeIdx>::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<typename N>
+Tree<N>::~Tree() = default;
+
+template<typename N>
+inline void Tree<N>::add_value(const Node& node, Float v)
+{
+    non_const(node).add_value(v);
+}
+
+template<typename N>
+inline void Tree<N>::add_value(const Node& node, Float v, Float weight)
+{
+    non_const(node).add_value(v, weight);
+}
+
+template<typename N>
+void Tree<N>::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<typename N>
+bool Tree<N>::contains(const Node& node) const
+{
+    return &node >= m_nodes.get() && &node < m_nodes.get() + m_max_nodes;
+}
+
+template<typename N>
+void Tree<N>::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<typename N>
+void Tree<N>::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<NodeIdx>(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<typename N>
+void Tree<N>::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<typename N>
+size_t Tree<N>::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<typename N>
+inline auto Tree<N>::get_root() const -> const Node&
+{
+    return m_nodes[0];
+}
+
+/** Get the thread storage a node belongs to. */
+template<typename N>
+inline unsigned Tree<N>::get_thread_storage(const Node& node) const
+{
+    size_t diff = &node - m_nodes.get();
+    return static_cast<unsigned>(diff / m_nodes_per_thread);
+}
+
+template<typename N>
+inline void Tree<N>::inc_visit_count(const Node& node)
+{
+    non_const(node).inc_visit_count();
+}
+
+template<typename N>
+inline void Tree<N>::link_children(const Node& node, const Node* first_child,
+                                   unsigned short nu_children)
+{
+    NodeIdx first_child_idx = static_cast<NodeIdx>(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<typename N>
+inline auto Tree<N>::non_const(const Node& node) const -> Node&
+{
+    LIBBOARDGAME_ASSERT(contains(node));
+    return const_cast<Node&>(node);
+}
+
+template<typename N>
+inline void Tree<N>::add_value_remove_loss(const Node& node, Float v)
+{
+    non_const(node).add_value_remove_loss(v);
+}
+
+template<typename N>
+void Tree<N>::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<ThreadStorage> m_thread_storage;
+        unique_ptr<Node[]> 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 (file)
index 0000000..6b64efe
--- /dev/null
@@ -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<typename N>
+const N* find_child(const Tree<N>& 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<typename N, class S>
+const N* find_node(const Tree<N>& 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 (file)
index 0000000..0cb8af9
--- /dev/null
@@ -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 (file)
index 0000000..7fc7e54
--- /dev/null
@@ -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<typename T>
+    InvalidPropertyValue(const string& id, const T& value);
+
+private:
+    template<typename T>
+    static string get_message(const string& id, const T& value);
+};
+
+template<typename T>
+InvalidPropertyValue::InvalidPropertyValue(const string& id, const T& value)
+    : InvalidTree(get_message(id, value))
+{
+}
+
+template<typename T>
+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 (file)
index 0000000..1bda509
--- /dev/null
@@ -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 <stdexcept>
+
+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 (file)
index 0000000..eeb3a4b
--- /dev/null
@@ -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 <config.h>
+#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 (file)
index 0000000..83fbb3c
--- /dev/null
@@ -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 (file)
index 0000000..f0004d7
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "Reader.h"
+
+#include <cctype>
+#include <cstdio>
+#include <fstream>
+#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<string>& 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 = &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 (file)
index 0000000..34d005d
--- /dev/null
@@ -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 <iosfwd>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+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<string>& 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<string> 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 (file)
index 0000000..5d0e148
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "SgfNode.h"
+
+#include <algorithm>
+#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<SgfNode> 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<SgfNode> 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<Property>::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<string> SgfNode::get_multi_property(const string& id) const
+{
+    auto property = find_property(id);
+    if (property == m_properties.end())
+        return vector<string>();
+    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<SgfNode> 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<Property>::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<SgfNode> 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<SgfNode> 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<SgfNode> 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<Property>::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> SgfNode::remove_child(SgfNode& child)
+{
+    auto node = &m_first_child;
+    unique_ptr<SgfNode>* previous = nullptr;
+    while (true)
+    {
+        if (node->get() == &child)
+        {
+            unique_ptr<SgfNode> 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 (file)
index 0000000..b7f0e60
--- /dev/null
@@ -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 <forward_list>
+#include <memory>
+#include <string>
+#include <vector>
+#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<string> values;
+
+    unique_ptr<Property> next;
+
+    Property(const Property& p)
+        : id(p.id),
+          values(p.values)
+    { }
+
+    Property(const string& id, const vector<string>& 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<SgfNode> 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<string> get_multi_property(const string& id) const;
+
+    /** Get property parsed as a type.
+        @pre has_property(id)
+        @throws InvalidPropertyValue, MissingProperty */
+    template<typename T>
+    T parse_property(const string& id) const;
+
+    /** Get property parsed as a type with default value.
+        @throws InvalidPropertyValue */
+    template<typename T>
+    T parse_property(const string& id, const T& default_value) const;
+
+    /** @return true, if property was added or changed. */
+    template<typename T>
+    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<typename T>
+    bool set_property(const string& id, const vector<T>& 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<Property>& 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<SgfNode> 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<SgfNode> 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<SgfNode> m_first_child;
+
+    unique_ptr<SgfNode> 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<Property> m_properties;
+
+    forward_list<Property>::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<bool>(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<typename T>
+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<typename T>
+T SgfNode::parse_property(const string& id, const T& default_value) const
+{
+    if (! has_property(id))
+        return default_value;
+    return parse_property<T>(id);
+}
+
+inline unique_ptr<SgfNode> SgfNode::remove_children()
+{
+    if (m_first_child)
+        m_first_child->m_parent = nullptr;
+    return move(m_first_child);
+}
+
+template<typename T>
+bool SgfNode::set_property(const string& id, const T& value)
+{
+    vector<T> values(1, value);
+    return set_property(id, values);
+}
+
+inline bool SgfNode::set_property(const string& id, const char* value)
+{
+    return set_property<string>(id, value);
+}
+
+template<typename T>
+bool SgfNode::set_property(const string& id, const vector<T>& values)
+{
+    vector<string> values_to_string;
+    for (const T& v : values)
+        values_to_string.push_back(to_string(v));
+    forward_list<Property>::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 (file)
index 0000000..f2c1886
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "SgfTree.h"
+
+#include <ctime>
+#include <cstdio>
+#include <cstdlib>
+#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<double>("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<double>("TE", 0);
+}
+
+unique_ptr<SgfNode> 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<SgfNode> root(new SgfNode);
+    m_root = move(root);
+    m_modified = false;
+}
+
+void SgfTree::init(unique_ptr<SgfNode>& 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 = &current->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<SgfNode> 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 (file)
index 0000000..8b9a4f9
--- /dev/null
@@ -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<SgfNode>& root);
+
+    /** Get the root node and transfer the ownership to the caller. */
+    unique_ptr<SgfNode> 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<typename T>
+    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<typename T>
+    void set_property(const SgfNode& node, const string& id,
+                      const vector<T>& 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<SgfNode> remove_children(const SgfNode& node);
+
+    void append(const SgfNode& node, unique_ptr<SgfNode> 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<SgfNode> m_root;
+
+    SgfNode& non_const(const SgfNode& node);
+};
+
+inline void SgfTree::append(const SgfNode& node, unique_ptr<SgfNode> 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<SgfNode&>(node);
+}
+
+inline unique_ptr<SgfNode> 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<typename T>
+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<typename T>
+void SgfTree::set_property(const SgfNode& node, const string& id,
+                        const vector<T>& 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 (file)
index 0000000..973ef5e
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "SgfUtil.h"
+
+#include <algorithm>
+#include <sstream>
+#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 = &current->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 = &current->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 = &current->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<const SgfNode*>& path)
+{
+    auto current = &node;
+    path.assign(1, current);
+    while(current->has_parent())
+    {
+        current = &current->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 (file)
index 0000000..d06c5f8
--- /dev/null
@@ -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 <string>
+#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<const SgfNode*>& 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 (file)
index 0000000..31553b7
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "TreeReader.h"
+
+namespace libboardgame_sgf {
+
+//-----------------------------------------------------------------------------
+
+TreeReader::TreeReader() = default;
+
+TreeReader::~TreeReader() = default;
+
+unique_ptr<SgfNode> 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<string>& 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 (file)
index 0000000..e79a994
--- /dev/null
@@ -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 <memory>
+#include <stack>
+#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<string>& values) override;
+
+    const SgfNode& get_tree() const;
+
+    /** Get the tree and transfer the ownership to the caller. */
+    unique_ptr<SgfNode> get_tree_transfer_ownership();
+
+private:
+    SgfNode* m_current = nullptr;
+
+    unique_ptr<SgfNode> m_root;
+
+    stack<SgfNode*> 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 (file)
index 0000000..48fc589
--- /dev/null
@@ -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 <config.h>
+#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<string>& 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 (file)
index 0000000..3e9af22
--- /dev/null
@@ -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<string>& 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 (file)
index 0000000..0efe0b1
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "Writer.h"
+
+#include <sstream>
+
+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 (file)
index 0000000..56c98da
--- /dev/null
@@ -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 <iosfwd>
+#include <string>
+#include <vector>
+#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<typename T>
+    void write_property(const string& id, const T& value);
+
+    template<typename T>
+    void write_property(const string& id, const vector<T>& 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<const char*> values(1, value);
+    write_property(id, values);
+}
+
+template<typename T>
+void Writer::write_property(const string& id, const T& value)
+{
+    vector<T> values(1, value);
+    write_property(id, values);
+}
+
+template<typename T>
+void Writer::write_property(const string& id, const vector<T>& 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<int>(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 (file)
index 0000000..a8990ce
--- /dev/null
@@ -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 (file)
index 0000000..70cda1b
--- /dev/null
@@ -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 <string>
+#include <typeinfo>
+#ifdef __GNUC__
+#include <cstdlib>
+#include <cxxabi.h>
+#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<typename T>
+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 (file)
index 0000000..2d9b0bc
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "CpuTime.h"
+
+#ifdef _WIN32
+#include <windows.h>
+#endif
+
+#if HAVE_UNISTD_H
+#include <unistd.h>
+#endif
+
+#if HAVE_SYS_TIMES_H
+#include <sys/times.h>
+#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 (file)
index 0000000..45b0f13
--- /dev/null
@@ -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 (file)
index 0000000..31fe4a7
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "Memory.h"
+
+#ifdef _WIN32
+#include <algorithm>
+#include <windows.h>
+#else
+#include <unistd.h>
+#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 <sys/sysctl.h>
+#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<size_t>(status.ullTotalVirtual);
+    auto total_phys = static_cast<size_t>(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<size_t>(phys_pages) * static_cast<size_t>(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 (file)
index 0000000..769aa1f
--- /dev/null
@@ -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 <cstddef>
+
+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 (file)
index 0000000..715549e
--- /dev/null
@@ -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 (file)
index 0000000..a1fafd4
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "Test.h"
+
+#include <sstream>
+#include <map>
+#include "libboardgame_util/Assert.h"
+#include "libboardgame_util/Log.h"
+
+namespace libboardgame_test {
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+map<string, TestFunction>& get_all_tests()
+{
+    static map<string, TestFunction> 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 (file)
index 0000000..4289341
--- /dev/null
@@ -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 <cmath>
+#include <sstream>
+#include <stdexcept>
+#include <string>
+
+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 (file)
index 0000000..ed1f426
--- /dev/null
@@ -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 (file)
index 0000000..2b9a586
--- /dev/null
@@ -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 (file)
index 0000000..f4e02bb
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "Abort.h"
+
+//----------------------------------------------------------------------------
+
+namespace libboardgame_util {
+
+using namespace std;
+
+atomic<bool> abort(false);
+
+//----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
diff --git a/src/libboardgame_util/Abort.h b/src/libboardgame_util/Abort.h
new file mode 100644 (file)
index 0000000..c5c3f94
--- /dev/null
@@ -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 <atomic>
+
+namespace libboardgame_util {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+extern atomic<bool> 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 (file)
index 0000000..d72cb46
--- /dev/null
@@ -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 <algorithm>
+#include <array>
+#include <initializer_list>
+#include <iostream>
+#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<typename T, unsigned M, typename I = unsigned>
+class ArrayList
+{
+public:
+    typedef I IntType;
+
+    static_assert(numeric_limits<IntType>::is_integer, "");
+
+    static const IntType max_size = M;
+
+    typedef typename array<T, max_size>::iterator iterator;
+
+    typedef typename array<T, max_size>::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<T>& 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<T, max_size> m_a;
+
+    I m_size = 0;
+};
+
+template<typename T, unsigned M, typename I>
+inline ArrayList<T, M, I>::ArrayList(const T& t)
+{
+    assign(t);
+}
+
+template<typename T, unsigned M, typename I>
+ArrayList<T, M, I>::ArrayList(const initializer_list<T>& l)
+    : m_size(0)
+{
+    for (auto& t : l)
+        push_back(t);
+}
+
+template<typename T, unsigned M, typename I>
+auto ArrayList<T, M, I>::operator=(const ArrayList& l) -> ArrayList&
+{
+    m_size = l.size();
+    copy(l.begin(), l.end(), begin());
+    return *this;
+}
+
+template<typename T, unsigned M, typename I>
+inline T& ArrayList<T, M, I>::operator[](I i)
+{
+    LIBBOARDGAME_ASSERT(i < m_size);
+    return m_a[i];
+}
+
+template<typename T, unsigned M, typename I>
+inline const T& ArrayList<T, M, I>::operator[](I i) const
+{
+    LIBBOARDGAME_ASSERT(i < m_size);
+    return m_a[i];
+}
+
+template<typename T, unsigned M, typename I>
+bool ArrayList<T, M, I>::operator==(const ArrayList& array_list) const
+{
+    if (m_size != array_list.m_size)
+        return false;
+    return equal(begin(), end(), array_list.begin());
+}
+
+template<typename T, unsigned M, typename I>
+bool ArrayList<T, M, I>::operator!=(const ArrayList& array_list) const
+{
+    return ! operator==(array_list);
+}
+
+template<typename T, unsigned M, typename I>
+inline void ArrayList<T, M, I>::assign(const T& t)
+{
+    m_size = 1;
+    m_a[0] = t;
+}
+
+template<typename T, unsigned M, typename I>
+inline T& ArrayList<T, M, I>::back()
+{
+    LIBBOARDGAME_ASSERT(m_size > 0);
+    return m_a[m_size - 1];
+}
+
+template<typename T, unsigned M, typename I>
+inline const T& ArrayList<T, M, I>::back() const
+{
+    LIBBOARDGAME_ASSERT(m_size > 0);
+    return m_a[m_size - 1];
+}
+
+template<typename T, unsigned M, typename I>
+inline auto ArrayList<T, M, I>::begin() -> iterator
+{
+    return m_a.begin();
+}
+
+template<typename T, unsigned M, typename I>
+inline auto ArrayList<T, M, I>::begin() const -> const_iterator
+{
+    return m_a.begin();
+}
+
+template<typename T, unsigned M, typename I>
+inline void ArrayList<T, M, I>::clear()
+{
+    m_size = 0;
+}
+
+template<typename T, unsigned M, typename I>
+bool ArrayList<T, M, I>::contains(const T& t) const
+{
+    return find(begin(), end(), t) != end();
+}
+
+template<typename T, unsigned M, typename I>
+inline bool ArrayList<T, M, I>::empty() const
+{
+    return m_size == 0;
+}
+
+template<typename T, unsigned M, typename I>
+inline auto ArrayList<T, M, I>::end() -> iterator
+{
+    return begin() + m_size;
+}
+
+template<typename T, unsigned M, typename I>
+inline auto ArrayList<T, M, I>::end() const -> const_iterator
+{
+    return begin() + m_size;
+}
+
+template<typename T, unsigned M, typename I>
+inline T& ArrayList<T, M, I>::get_unchecked(I i)
+{
+    LIBBOARDGAME_ASSERT(i < max_size);
+    return m_a[i];
+}
+
+template<typename T, unsigned M, typename I>
+inline const T& ArrayList<T, M, I>::get_unchecked(I i) const
+{
+    LIBBOARDGAME_ASSERT(i < max_size);
+    return m_a[i];
+}
+
+template<typename T, unsigned M, typename I>
+bool ArrayList<T, M, I>::include(const T& t)
+{
+    if (contains(t))
+        return false;
+    push_back(t);
+    return true;
+}
+
+template<typename T, unsigned M, typename I>
+inline const T& ArrayList<T, M, I>::pop_back()
+{
+    LIBBOARDGAME_ASSERT(m_size > 0);
+    return m_a[--m_size];
+}
+
+template<typename T, unsigned M, typename I>
+inline void ArrayList<T, M, I>::push_back(const T& t)
+{
+    LIBBOARDGAME_ASSERT(m_size < max_size);
+    m_a[m_size++] = t;
+}
+
+template<typename T, unsigned M, typename I>
+inline bool ArrayList<T, M, I>::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<typename T, unsigned M, typename I>
+inline bool ArrayList<T, M, I>::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<typename T, unsigned M, typename I>
+inline void ArrayList<T, M, I>::remove_fast(iterator i)
+{
+    LIBBOARDGAME_ASSERT(i >= begin());
+    LIBBOARDGAME_ASSERT(i < end());
+    --m_size;
+    *i = *(begin() + m_size);
+}
+
+template<typename T, unsigned M, typename I>
+inline void ArrayList<T, M, I>::resize(I size)
+{
+    LIBBOARDGAME_ASSERT(size <= max_size);
+    m_size = size;
+}
+
+template<typename T, unsigned M, typename I>
+inline I ArrayList<T, M, I>::size() const
+{
+    return m_size;
+}
+
+//-----------------------------------------------------------------------------
+
+template<typename T, unsigned M, typename I>
+ostream& operator<<(ostream& out, const ArrayList<T, M, I>& 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 (file)
index 0000000..91d6617
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "Assert.h"
+
+#include <list>
+
+#if LIBBOARDGAME_DEBUG
+#include <algorithm>
+#include <functional>
+#include <sstream>
+#include <string>
+#include <vector>
+#include "Log.h"
+#endif
+
+namespace libboardgame_util {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+list<AssertionHandler*>& get_all_handlers()
+{
+    static list<AssertionHandler*> 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 (file)
index 0000000..cd696fe
--- /dev/null
@@ -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<void>(0))                                    \
+     : libboardgame_util::handle_assertion(#expr, __FILE__, __LINE__))
+#else
+#define LIBBOARDGAME_ASSERT(expr) (static_cast<void>(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 (file)
index 0000000..cd4a76f
--- /dev/null
@@ -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 <config.h>
+#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<mutex> 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 (file)
index 0000000..f96cef8
--- /dev/null
@@ -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 <condition_variable>
+#include <mutex>
+
+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 (file)
index 0000000..076e97d
--- /dev/null
@@ -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 (file)
index 0000000..e38be00
--- /dev/null
@@ -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 <config.h>
+#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 (file)
index 0000000..b4ad8de
--- /dev/null
@@ -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 (file)
index 0000000..a832e0f
--- /dev/null
@@ -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 <iostream>
+
+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 (file)
index 0000000..520bbdf
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "IntervalChecker.h"
+
+#include <limits>
+#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<bool()> 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<unsigned>::max()))
+            m_count_interval = numeric_limits<unsigned>::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 (file)
index 0000000..fcef4e1
--- /dev/null
@@ -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 <functional>
+#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<bool()> 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<bool()> 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 (file)
index 0000000..c73a0a7
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "Log.h"
+
+#include <iostream>
+
+#if defined ANDROID || defined __ANDROID__
+#include <android/log.h>
+#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 (file)
index 0000000..617fd32
--- /dev/null
@@ -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 <sstream>
+#include <string>
+
+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<typename T>
+void _log_buffered(ostream& buffer, const T& t)
+{
+    buffer << t;
+}
+
+/** Helper function needed for log(const Ts&...) */
+template<typename T, typename... Ts>
+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<typename... Ts>
+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<void>(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 (file)
index 0000000..4342c0a
--- /dev/null
@@ -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<typename T>
+inline T fast_exp(T x)
+{
+    x = static_cast<T>(1) + x / static_cast<T>(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 (file)
index 0000000..4f1a3f9
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "Options.h"
+
+namespace libboardgame_util {
+
+//----------------------------------------------------------------------------
+
+Options::Options(int argc, const char** argv, const vector<string>& 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<string>& specs)
+    : Options(argc, const_cast<const char**>(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 (file)
index 0000000..4f3504e
--- /dev/null
@@ -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 <map>
+#include <set>
+#include <stdexcept>
+#include <string>
+#include <vector>
+#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<string>& 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<string>& 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<typename T>
+    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<typename T>
+    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<string>& get_args() const;
+
+private:
+    set<string> m_names;
+
+    vector<string> m_args;
+
+    map<string, string> m_map;
+
+    void check_name(const string& name) const;
+};
+
+template<typename T>
+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<typename T>
+T Options::get(const string& name, const T& default_value) const
+{
+    if (! contains(name))
+        return default_value;
+    return get<T>(name);
+}
+
+inline const vector<string>& 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 (file)
index 0000000..737ca93
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "RandomGenerator.h"
+
+#include <list>
+
+namespace libboardgame_util {
+
+//----------------------------------------------------------------------------
+
+namespace {
+
+bool is_seed_set = false;
+
+RandomGenerator::ResultType the_seed;
+
+list<RandomGenerator*>& get_all_generators()
+{
+    static list<RandomGenerator*> 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 (file)
index 0000000..4f00966
--- /dev/null
@@ -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 <random>
+
+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<double> distribution(a, b);
+    return distribution(m_generator);
+}
+
+inline float RandomGenerator::generate_float(float a, float b)
+{
+    uniform_real_distribution<float> 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 (file)
index 0000000..b206f34
--- /dev/null
@@ -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 <cstddef>
+
+namespace libboardgame_util {
+
+//-----------------------------------------------------------------------------
+
+template<typename T>
+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<typename T>
+bool Range<T>::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 (file)
index 0000000..02e4b6b
--- /dev/null
@@ -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 <atomic>
+#include <cmath>
+#include <iomanip>
+#include <iosfwd>
+#include <limits>
+#include <sstream>
+#include <string>
+#include "FmtSaver.h"
+
+namespace libboardgame_util {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+template<typename FLOAT = double>
+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<typename FLOAT>
+inline StatisticsBase<FLOAT>::StatisticsBase(FLOAT init_val)
+{
+    clear(init_val);
+}
+
+template<typename FLOAT>
+void StatisticsBase<FLOAT>::add(FLOAT val)
+{
+    FLOAT count = m_count;
+    ++count;
+    val -= m_mean;
+    m_mean +=  val / count;
+    m_count = count;
+}
+
+template<typename FLOAT>
+inline void StatisticsBase<FLOAT>::clear(FLOAT init_val)
+{
+    m_count = 0;
+    m_mean = init_val;
+}
+
+template<typename FLOAT>
+inline FLOAT StatisticsBase<FLOAT>::get_count() const
+{
+    return m_count;
+}
+
+template<typename FLOAT>
+inline FLOAT StatisticsBase<FLOAT>::get_mean() const
+{
+    return m_mean;
+}
+
+template<typename FLOAT>
+void StatisticsBase<FLOAT>::write(ostream& out, bool fixed,
+                                  unsigned precision) const
+{
+    FmtSaver saver(out);
+    if (fixed)
+        out << std::fixed;
+    out << setprecision(precision) << m_mean;
+}
+
+//----------------------------------------------------------------------------
+
+template<typename FLOAT = double>
+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<FLOAT> m_statistics_base;
+
+    FLOAT m_variance;
+};
+
+template<typename FLOAT>
+inline Statistics<FLOAT>::Statistics(FLOAT init_val)
+{
+    clear(init_val);
+}
+
+template<typename FLOAT>
+void Statistics<FLOAT>::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<typename FLOAT>
+inline void Statistics<FLOAT>::clear(FLOAT init_val)
+{
+    m_statistics_base.clear(init_val);
+    m_variance = 0;
+}
+
+template<typename FLOAT>
+inline FLOAT Statistics<FLOAT>::get_count() const
+{
+    return m_statistics_base.get_count();
+}
+
+template<typename FLOAT>
+inline FLOAT Statistics<FLOAT>::get_deviation() const
+{
+    // m_variance can become negative (due to rounding errors?)
+    return m_variance < 0 ? 0 : sqrt(m_variance);
+}
+
+template<typename FLOAT>
+FLOAT Statistics<FLOAT>::get_error() const
+{
+    auto count = get_count();
+    return count == 0 ? 0 : get_deviation() / sqrt(count);
+}
+
+template<typename FLOAT>
+inline FLOAT Statistics<FLOAT>::get_mean() const
+{
+    return m_statistics_base.get_mean();
+}
+
+template<typename FLOAT>
+inline FLOAT Statistics<FLOAT>::get_variance() const
+{
+    return m_variance;
+}
+
+template<typename FLOAT>
+void Statistics<FLOAT>::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<typename FLOAT = double>
+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<FLOAT> m_statistics;
+
+    FLOAT m_max;
+
+    FLOAT m_min;
+};
+
+template<typename FLOAT>
+inline StatisticsExt<FLOAT>::StatisticsExt(FLOAT init_val)
+{
+    clear(init_val);
+}
+
+template<typename FLOAT>
+void StatisticsExt<FLOAT>::add(FLOAT val)
+{
+    m_statistics.add(val);
+    if (val > m_max)
+        m_max = val;
+    if (val < m_min)
+        m_min = val;
+}
+
+template<typename FLOAT>
+inline void StatisticsExt<FLOAT>::clear(FLOAT init_val)
+{
+    m_statistics.clear(init_val);
+    m_min = numeric_limits<FLOAT>::max();
+    m_max = -numeric_limits<FLOAT>::max();
+}
+
+template<typename FLOAT>
+inline FLOAT StatisticsExt<FLOAT>::get_count() const
+{
+    return m_statistics.get_count();
+}
+
+template<typename FLOAT>
+inline FLOAT StatisticsExt<FLOAT>::get_deviation() const
+{
+    return m_statistics.get_deviation();
+}
+
+template<typename FLOAT>
+inline FLOAT StatisticsExt<FLOAT>::get_error() const
+{
+    return m_statistics.get_error();
+}
+
+template<typename FLOAT>
+inline FLOAT StatisticsExt<FLOAT>::get_max() const
+{
+    return m_max;
+}
+
+template<typename FLOAT>
+inline FLOAT StatisticsExt<FLOAT>::get_mean() const
+{
+    return m_statistics.get_mean();
+}
+
+template<typename FLOAT>
+inline FLOAT StatisticsExt<FLOAT>::get_min() const
+{
+    return m_min;
+}
+
+template<typename FLOAT>
+inline FLOAT StatisticsExt<FLOAT>::get_variance() const
+{
+    return m_statistics.get_variance();
+}
+
+template<typename FLOAT>
+string StatisticsExt<FLOAT>::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<typename FLOAT>
+void StatisticsExt<FLOAT>::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<FLOAT>::max())
+        out << "-";
+    else
+        out << m_min;
+    out << " max=";
+    if (m_max == -numeric_limits<FLOAT>::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<typename FLOAT = double>
+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<FLOAT> m_count;
+
+    atomic<FLOAT> m_mean;
+};
+
+template<typename FLOAT>
+inline StatisticsDirtyLockFree<FLOAT>::StatisticsDirtyLockFree(FLOAT init_val)
+{
+    clear(init_val);
+}
+
+template<typename FLOAT>
+StatisticsDirtyLockFree<FLOAT>&
+StatisticsDirtyLockFree<FLOAT>::operator=(const StatisticsDirtyLockFree& s)
+{
+    m_count = s.m_count.load();
+    m_mean = s.m_mean.load();
+    return *this;
+}
+
+template<typename FLOAT>
+void StatisticsDirtyLockFree<FLOAT>::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<typename FLOAT>
+inline void StatisticsDirtyLockFree<FLOAT>::clear(FLOAT init_val)
+{
+    init(init_val, 0);
+}
+
+template<typename FLOAT>
+inline FLOAT StatisticsDirtyLockFree<FLOAT>::get_count() const
+{
+    return m_count.load(memory_order_relaxed);
+}
+
+template<typename FLOAT>
+inline FLOAT StatisticsDirtyLockFree<FLOAT>::get_mean() const
+{
+    return m_mean.load(memory_order_relaxed);
+}
+
+template<typename FLOAT>
+inline void StatisticsDirtyLockFree<FLOAT>::init(FLOAT mean, FLOAT count)
+{
+    m_count = count;
+    m_mean = mean;
+}
+
+template<typename FLOAT>
+void StatisticsDirtyLockFree<FLOAT>::write(ostream& out, bool fixed,
+                                           unsigned precision) const
+{
+    FmtSaver saver(out);
+    if (fixed)
+        out << std::fixed;
+    out << setprecision(precision) << get_mean();
+}
+
+//----------------------------------------------------------------------------
+
+template<typename FLOAT>
+inline ostream& operator<<(ostream& out, const StatisticsExt<FLOAT>& 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 (file)
index 0000000..f95529a
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "StringUtil.h"
+
+#include <cctype>
+#include <iomanip>
+
+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<string> split(const string& s, char separator)
+{
+    vector<string> 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<char>(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 (file)
index 0000000..65fb83b
--- /dev/null
@@ -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 <sstream>
+#include <string>
+#include <vector>
+
+namespace libboardgame_util {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+template<typename T>
+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<string> split(const string& s, char separator);
+
+string time_to_string(double seconds, bool with_seconds_as_double = false);
+
+template<typename T>
+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 (file)
index 0000000..0877e6d
--- /dev/null
@@ -0,0 +1,43 @@
+//-----------------------------------------------------------------------------
+/** @file TimeIntervalChecker.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#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 (file)
index 0000000..d898b02
--- /dev/null
@@ -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 (file)
index 0000000..43f2a24
--- /dev/null
@@ -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 <config.h>
+#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 (file)
index 0000000..bc040fd
--- /dev/null
@@ -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 (file)
index 0000000..68a8a3e
--- /dev/null
@@ -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 <config.h>
+#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 (file)
index 0000000..13a23b9
--- /dev/null
@@ -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 (file)
index 0000000..43be3cf
--- /dev/null
@@ -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<class T> 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 (file)
index 0000000..754e20f
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "WallTimeSource.h"
+
+#include <chrono>
+
+namespace libboardgame_util {
+
+using namespace std::chrono;
+
+//-----------------------------------------------------------------------------
+
+double WallTimeSource::operator()()
+{
+    auto t = system_clock::now().time_since_epoch();
+    return duration_cast<duration<double>>(t).count();
+}
+
+//----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
diff --git a/src/libboardgame_util/WallTimeSource.h b/src/libboardgame_util/WallTimeSource.h
new file mode 100644 (file)
index 0000000..9c99371
--- /dev/null
@@ -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 (file)
index 0000000..a06ddcf
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "Board.h"
+
+#include <functional>
+#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<decltype(m_snapshot.moves_size)>::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<ScoreType, Color::range> 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<ScoreType>());
+    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<uint_fast8_t>(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<PiecesLeftList> 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<PiecesLeftList>& 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 (file)
index 0000000..9217832
--- /dev/null
@@ -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<PointState> PointStateGrid;
+
+    /** Maximum number of pieces per player in any game variant. */
+    static const unsigned max_pieces = Setup::max_pieces;
+
+    typedef ArrayList<Piece, Piece::max_pieces> 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<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+    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<ColorMove, max_game_moves>& 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<bool>& 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<const Point> 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<Point,StartingPoints::max_starting_points>&
+    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<bool> forbidden;
+
+        Grid<bool> is_attach_point;
+
+        PiecesLeftList pieces_left;
+
+        PieceMap<uint_fast8_t> nu_left_piece;
+
+        unsigned nu_onboard_pieces;
+
+        ScoreType points;
+    };
+
+    /** Snapshot for fast restoration of a previous position. */
+    struct Snapshot
+    {
+        StateBase state_base;
+
+        ColorMap<StateColor> state_color;
+
+        unsigned moves_size;
+
+        ColorMap<unsigned> attach_points_size;
+    };
+
+
+    StateBase m_state_base;
+
+    ColorMap<StateColor> 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<ScoreType> 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<bool> m_is_center_section;
+
+    /** The 1x1 piece. */
+    Piece m_one_piece;
+
+    ColorMap<PointList> m_attach_points;
+
+    /** See get_second_color() */
+    ColorMap<Color> m_second_color;
+
+    ColorMap<char> m_color_char;
+
+    ColorMap<const char*> m_color_esc_sequence;
+
+    ColorMap<const char*> m_color_esc_sequence_text;
+
+    ColorMap<const char*> m_color_name;
+
+    ArrayList<ColorMove, max_game_moves> 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<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+    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<PiecesLeftList>& 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<Color::IntType>(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<const Point> Board::get_move_points(Move mv) const
+{
+    return m_bc->get_move_points(mv);
+}
+
+inline auto Board::get_moves() const
+-> const ArrayList<ColorMove, max_game_moves>&
+{
+    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<Color::IntType>(m_nu_players);
+    for (Color i : get_colors())
+        if (i != c)
+            score -= get_points(i);
+    score = get_points(c) + score / (static_cast<ScoreType>(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<Point,StartingPoints::max_starting_points>&
+                                       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<bool>& 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<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+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<MAX_SIZE>(mv, m_move_info_array);
+    auto& info_ext = BoardConst::get_move_info_ext<MAX_ADJ_ATTACH>(
+                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<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+inline void Board::play(Color c, Move mv)
+{
+    place<MAX_SIZE, MAX_ADJ_ATTACH>(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 (file)
index 0000000..3ca651b
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "BoardConst.h"
+
+#include <algorithm>
+#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<array<ArrayList<Move, 40>, 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<PieceInfo> create_pieces_callisto(const Geometry& geo,
+                                         PieceSet piece_set,
+                                         const PieceTransforms& transforms)
+{
+    vector<PieceInfo> 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<PieceInfo> create_pieces_classic(const Geometry& geo,
+                                        PieceSet piece_set,
+                                        const PieceTransforms& transforms)
+{
+    vector<PieceInfo> 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<PieceInfo> create_pieces_junior(const Geometry& geo,
+                                       PieceSet piece_set,
+                                       const PieceTransforms& transforms)
+{
+    vector<PieceInfo> 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<PieceInfo> create_pieces_trigon(const Geometry& geo,
+                                       PieceSet piece_set,
+                                       const PieceTransforms& transforms)
+{
+    vector<PieceInfo> 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<PieceInfo> create_pieces_nexos(const Geometry& geo,
+                                      PieceSet piece_set,
+                                      const PieceTransforms& transforms)
+{
+    vector<PieceInfo> 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<Piece::IntType>(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<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+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<Move::IntType>(moves_created));
+    void* place =
+            static_cast<MoveInfo<MAX_SIZE>*>(m_move_info.get())
+            + moves_created;
+    new(place) MoveInfo<MAX_SIZE>(piece, points);
+    place =
+            static_cast<MoveInfoExt<MAX_ADJ_ATTACH>*>(m_move_info_ext.get())
+            + moves_created;
+    auto& info_ext = *new(place) MoveInfoExt<MAX_ADJ_ATTACH>();
+    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<uint_least8_t>(
+                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<uint_least8_t>(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<uint_least8_t>(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<unsigned>(info_ext.size_attach_points));
+    if (log_move_creation)
+    {
+        Grid<char> 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<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+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<PiecePoints> transformed_points(nu_transforms);
+    vector<CoordPoint> 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<MovePoints::IntType>(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<MAX_SIZE, MAX_ADJ_ATTACH>(
+                        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<string> 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<BoardType, map<PieceSet, unique_ptr<BoardConst>>> 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<unsigned MAX_SIZE>
+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<MAX_SIZE>(mv);
+        auto& info_ext_2 = m_move_info_ext_2[i];
+        info_ext_2.breaks_symmetry = false;
+        array<Point, PieceInfo::max_size> 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<MAX_SIZE>(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 (file)
index 0000000..fb15cdb
--- /dev/null
@@ -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<Point, PrecompMoves::adj_status_nu_adj, unsigned short>
+    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<unsigned MAX_SIZE>
+    static const MoveInfo<MAX_SIZE>&
+    get_move_info(Move mv, MoveInfoArray move_info_array);
+
+    template<unsigned MAX_ADJ_ATTACH>
+    static const MoveInfoExt<MAX_ADJ_ATTACH>&
+    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<const Point> 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<unsigned MAX_SIZE>
+    const Point* get_move_points_begin(Move mv) const;
+
+    Piece get_move_piece(Move mv) const;
+
+    template<unsigned MAX_SIZE>
+    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<PieceInfo> m_pieces;
+
+    Grid<AdjStatusList> m_adj_status_list;
+
+    unique_ptr<PieceTransforms> m_transforms;
+
+    PieceMap<unsigned> m_nu_attach_points{0};
+
+    /** Array of MoveInfo<MAX_SIZE> with MAX_SIZE being the maximum piece size
+        in the corresponding game variant.
+        See comments at MoveInfo. */
+    unique_ptr<void, MallocFree> m_move_info;
+
+    /** Array of MoveInfoExt<MAX_ADJ_ATTACH> 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<void, MallocFree> m_move_info_ext;
+
+    unique_ptr<MoveInfoExt2[]> 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<unsigned> m_compare_val;
+
+    SymmetricPoints m_symmetric_points;
+
+
+    BoardConst(BoardType board_type, PieceSet piece_set);
+
+    template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+    void create_move(unsigned& moves_created, Piece piece,
+                     const MovePoints& points, Point label_pos);
+
+    void create_moves();
+
+    template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+    void create_moves(unsigned& moves_created, Piece piece);
+
+    template<unsigned MAX_SIZE>
+    const MoveInfo<MAX_SIZE>& get_move_info(Move mv) const;
+
+    void init_adj_status();
+
+    template<unsigned MAX_SIZE>
+    void init_symmetry_info();
+};
+
+inline const Geometry& BoardConst::get_geometry() const
+{
+    return m_geo;
+}
+
+template<unsigned MAX_SIZE>
+inline const MoveInfo<MAX_SIZE>&
+BoardConst::get_move_info(Move mv, MoveInfoArray move_info_array)
+{
+    LIBBOARDGAME_ASSERT(! mv.is_null());
+    return *(static_cast<const MoveInfo<MAX_SIZE>*>(move_info_array)
+             + mv.to_int());
+}
+
+template<unsigned MAX_SIZE>
+inline const MoveInfo<MAX_SIZE>& BoardConst::get_move_info(Move mv) const
+{
+    LIBBOARDGAME_ASSERT(m_max_piece_size == MAX_SIZE);
+    return get_move_info<MAX_SIZE>(mv, m_move_info.get());
+}
+
+template<unsigned MAX_ADJ_ATTACH>
+inline const MoveInfoExt<MAX_ADJ_ATTACH>&
+BoardConst::get_move_info_ext(Move mv, MoveInfoExtArray move_info_ext_array)
+{
+    LIBBOARDGAME_ASSERT(! mv.is_null());
+    return *(static_cast<const MoveInfoExt<MAX_ADJ_ATTACH>*>(
+                 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<unsigned MAX_SIZE>
+inline Piece BoardConst::get_move_piece(Move mv) const
+{
+    return get_move_info<MAX_SIZE>(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<const Point> BoardConst::get_move_points(Move mv) const
+{
+    if (m_max_piece_size == 5)
+    {
+        auto& info = get_move_info<5>(mv);
+        return Range<const Point>(info.begin(), info.end());
+    }
+    else if (m_max_piece_size == 6)
+    {
+        auto& info = get_move_info<6>(mv);
+        return Range<const Point>(info.begin(), info.end());
+    }
+    else
+    {
+        LIBBOARDGAME_ASSERT(m_max_piece_size == 7);
+        auto& info = get_move_info<7>(mv);
+        return Range<const Point>(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<unsigned MAX_SIZE>
+inline const Point* BoardConst::get_move_points_begin(Move mv) const
+{
+    return get_move_info<MAX_SIZE>(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 (file)
index 0000000..2a275b6
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "BoardUpdater.h"
+
+#include <stdexcept>
+#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<Piece, PieceInfo::max_instances * Piece::max_pieces>
+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<AllPiecesLeftList>& pieces_left)
+{
+    if (! node.has_property(id))
+        return;
+    vector<string> 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<AllPiecesLeftList>& pieces_left)
+{
+    if (! node.has_property("AE"))
+        return;
+    vector<string> 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<AllPiecesLeftList> 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 (file)
index 0000000..1925313
--- /dev/null
@@ -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<const SgfNode*> 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 (file)
index 0000000..7e819a6
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "BoardUtil.h"
+
+#include "PentobiSgfUtil.h"
+#if LIBBOARDGAME_DEBUG
+#include <sstream>
+#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<Point>& 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<string> 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 (file)
index 0000000..e99717a
--- /dev/null
@@ -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<Point>& 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 (file)
index 0000000..58c9078
--- /dev/null
@@ -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 <config.h>
+#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<SgfNode> 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<const SgfNode*> 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<unsigned>(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 (file)
index 0000000..0ab77ea
--- /dev/null
@@ -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 <iosfwd>
+#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<Point> PointTransform;
+
+    PentobiTree m_tree;
+
+    RandomGenerator m_random;
+
+    vector<unique_ptr<PointTransform>> m_transforms;
+
+    vector<unique_ptr<PointTransform>> 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 (file)
index 0000000..24ad32f
--- /dev/null
@@ -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 (file)
index 0000000..68dc38c
--- /dev/null
@@ -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 <config.h>
+#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<unsigned, shared_ptr<CallistoGeometry>> 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<CallistoGeometry> 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 (file)
index 0000000..f53cf8d
--- /dev/null
@@ -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 <map>
+#include <memory>
+#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<unsigned, shared_ptr<CallistoGeometry>> 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 (file)
index 0000000..e5869a9
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "Color.h"
+
+#include <sstream>
+#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 (file)
index 0000000..3d7a0da
--- /dev/null
@@ -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 <cstdint>
+#include <iosfwd>
+#include <stdexcept>
+#include <string>
+#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<IntType>(m_i + 1) % nu_colors);
+}
+
+inline Color Color::get_previous(IntType nu_colors) const
+{
+    return Color(static_cast<IntType>(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<class FUNCTION>
+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 (file)
index 0000000..66b3610
--- /dev/null
@@ -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 <array>
+#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<typename T>
+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<T, Color::range> m_a;
+};
+
+template<typename T>
+inline ColorMap<T>::ColorMap(const T& val)
+{
+    fill(val);
+}
+
+template<typename T>
+inline T& ColorMap<T>::operator[](Color c)
+{
+    return m_a[c.to_int()];
+}
+
+template<typename T>
+inline const T& ColorMap<T>::operator[](Color c) const
+{
+    return m_a[c.to_int()];
+}
+
+template<typename T>
+void ColorMap<T>::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 (file)
index 0000000..3bbe2e8
--- /dev/null
@@ -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 (file)
index 0000000..2b49931
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "Engine.h"
+
+#include <fstream>
+#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<MoveList> moves(new MoveList);
+    unique_ptr<MoveMarker> 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<int>(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<Move::IntType>());
+    }
+    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<int>(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<bool>(1);
+        else if (name == "resign")
+            m_resign = args.parse<bool>(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<int> 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
+    <br>
+    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 (file)
index 0000000..d4e1493
--- /dev/null
@@ -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 (file)
index 0000000..ce39236
--- /dev/null
@@ -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 <config.h>
+#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<SgfNode>& 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 (file)
index 0000000..0f5fa5c
--- /dev/null
@@ -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<SgfNode>& 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<Board> 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 (file)
index 0000000..cff9b28
--- /dev/null
@@ -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<Point> 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 (file)
index 0000000..78d4e72
--- /dev/null
@@ -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<typename T>
+using Grid = libboardgame_base::Grid<Point, T>;
+
+template<typename T>
+using GridExt = libboardgame_base::GridExt<Point, T>;
+
+//-----------------------------------------------------------------------------
+
+} // 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 (file)
index 0000000..eb867c9
--- /dev/null
@@ -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<Point> 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 (file)
index 0000000..1022ca6
--- /dev/null
@@ -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 <cstdint>
+#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 (file)
index 0000000..3cd8921
--- /dev/null
@@ -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<unsigned MAX_SIZE>
+class MoveInfo
+{
+public:
+    MoveInfo() = default;
+
+    MoveInfo(Piece piece, const MovePoints& points)
+    {
+        m_piece = static_cast<uint_least8_t>(piece.to_int());
+        m_size = static_cast<uint_least8_t>(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<unsigned MAX_ADJ_ATTACH>
+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 (file)
index 0000000..9f278e9
--- /dev/null
@@ -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<Move, Move::range - 1> 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 (file)
index 0000000..5e205af
--- /dev/null
@@ -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 <array>
+#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<class T>
+    void set(const T& t)
+    {
+        for (Move mv : t)
+            set(mv);
+    }
+
+    template<class T>
+    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<bool, Move::range> 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 (file)
index 0000000..1383234
--- /dev/null
@@ -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<Point, PieceInfo::max_size, unsigned short> 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 (file)
index 0000000..29df080
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "NexosGeometry.h"
+
+#include "libboardgame_util/Unused.h"
+
+namespace libpentobi_base {
+
+using libboardgame_base::CoordPoint;
+
+//-----------------------------------------------------------------------------
+
+map<unsigned, shared_ptr<NexosGeometry>> 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<NexosGeometry> 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 (file)
index 0000000..58c7909
--- /dev/null
@@ -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 <map>
+#include <memory>
+#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:
+    <tt>
+      0 1 2 3 4 5 6 ...
+    0 + - + - + - +
+    1 |   |   |   |
+    2 + - + - + - +
+    3 |   |   |   |
+    4 + - + - + - +
+    </tt>
+    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<unsigned, shared_ptr<NexosGeometry>> 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 (file)
index 0000000..1b7b513
--- /dev/null
@@ -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 <config.h>
+#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<string> 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<string> 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 (file)
index 0000000..dab324d
--- /dev/null
@@ -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 (file)
index 0000000..6a09123
--- /dev/null
@@ -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 <config.h>
+#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 (file)
index 0000000..dc6111e
--- /dev/null
@@ -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 (file)
index 0000000..b684b5e
--- /dev/null
@@ -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 <config.h>
+#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<SgfNode>& 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<string> 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<SgfNode>& 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<Board> 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<string> 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 (file)
index 0000000..213280a
--- /dev/null
@@ -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<SgfNode>& root);
+
+    void init(unique_ptr<SgfNode>& 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 (file)
index 0000000..660b4b3
--- /dev/null
@@ -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 <config.h>
+#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<string>& 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<string> 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 (file)
index 0000000..bd463a1
--- /dev/null
@@ -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<string>& 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 (file)
index 0000000..1aa3a29
--- /dev/null
@@ -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 (file)
index 0000000..663fb35
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "PieceInfo.h"
+
+#include <algorithm>
+#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<NormalizedPoints> 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<ScoreType>(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 (file)
index 0000000..416fb5f
--- /dev/null
@@ -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 <map>
+#include <string>
+#include <vector>
+#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<CoordPoint, max_size> 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<const Transform*>& 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<const Transform*> m_uniq_transforms;
+
+    map<const Transform*,const Transform*> 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 (file)
index 0000000..67930c6
--- /dev/null
@@ -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 <array>
+#include <algorithm>
+#include "Piece.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+/** Container mapping a unique piece to another element type.
+    The elements must be default-constructible. */
+template<typename T>
+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<T, Piece::range_not_null> m_a;
+};
+
+template<typename T>
+inline PieceMap<T>::PieceMap(const T& val)
+{
+    fill(val);
+}
+
+template<typename T>
+PieceMap<T>& PieceMap<T>::operator=(const PieceMap& piece_map)
+{
+    copy(piece_map.m_a.begin(), piece_map.m_a.end(), m_a.begin());
+    return *this;
+}
+
+template<typename T>
+bool PieceMap<T>::operator==(const PieceMap& piece_map) const
+{
+    return equal(m_a.begin(), m_a.end(), piece_map.m_a.begin());
+}
+
+template<typename T>
+inline T& PieceMap<T>::operator[](Piece piece)
+{
+    LIBBOARDGAME_ASSERT(! piece.is_null());
+    return m_a[piece.to_int()];
+}
+
+template<typename T>
+inline const T& PieceMap<T>::operator[](Piece piece) const
+{
+    LIBBOARDGAME_ASSERT(! piece.is_null());
+    return m_a[piece.to_int()];
+}
+
+template<typename T>
+void PieceMap<T>::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 (file)
index 0000000..c67551e
--- /dev/null
@@ -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 <config.h>
+#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 (file)
index 0000000..da6a4cb
--- /dev/null
@@ -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 <vector>
+#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<const Transform*>& 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<class T>
+    const Transform* find() const;
+
+protected:
+    /** All piece transformations.
+        Must be initialized in constructor of subclass. */
+    vector<const Transform*> m_all;
+};
+
+template<class T>
+const Transform* PieceTransforms::find() const
+{
+    for (auto t : m_all)
+        if (dynamic_cast<const T*>(t))
+            return t;
+    return nullptr;
+}
+
+inline const Transform* PieceTransforms::get_default() const
+{
+    return m_all[0];
+}
+
+inline const vector<const Transform*>& 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 (file)
index 0000000..2186613
--- /dev/null
@@ -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 <config.h>
+#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 (file)
index 0000000..e466845
--- /dev/null
@@ -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 (file)
index 0000000..2b0f2d3
--- /dev/null
@@ -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 <config.h>
+#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 (file)
index 0000000..47d94d0
--- /dev/null
@@ -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 (file)
index 0000000..d6be112
--- /dev/null
@@ -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 <config.h>
+#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 (file)
index 0000000..df9b92a
--- /dev/null
@@ -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 (file)
index 0000000..6b3dae9
--- /dev/null
@@ -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 (file)
index 0000000..4cfab9e
--- /dev/null
@@ -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<Point, Point::max_onboard> 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 (file)
index 0000000..de1267b
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "PointState.h"
+
+#include <iostream>
+
+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 (file)
index 0000000..a950b51
--- /dev/null
@@ -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 (file)
index 0000000..60a05a9
--- /dev/null
@@ -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<const Move> 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<array<PieceMap<CompressedRange>, 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<Move, max_move_lists_sum_length> 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 (file)
index 0000000..a2a661e
--- /dev/null
@@ -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 <algorithm>
+#include <array>
+#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<typename FLOAT>
+void get_multiplayer_result(unsigned nu_players,
+                            const array<ScoreType, Color::range>& points,
+                            array<FLOAT, Color::range>& result,
+                            bool break_ties)
+{
+    array<ScoreType, Color::range> 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 (file)
index 0000000..2a74c64
--- /dev/null
@@ -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<Move,max_pieces> PlacementList;
+
+    Color to_play = Color(0);
+
+    ColorMap<PlacementList> 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 (file)
index 0000000..6c61f01
--- /dev/null
@@ -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 <config.h>
+#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 (file)
index 0000000..acd748f
--- /dev/null
@@ -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<Point,StartingPoints::max_starting_points>&
+                                             get_starting_points(Color c) const;
+
+private:
+    Grid<bool> m_is_colored_starting_point;
+
+    Grid<bool> m_is_colorless_starting_point;
+
+    Grid<Color> m_starting_point_color;
+
+    ColorMap<ArrayList<Point,max_starting_points>> 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<Point,StartingPoints::max_starting_points>&
+                              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 (file)
index 0000000..b392ad6
--- /dev/null
@@ -0,0 +1,31 @@
+//-----------------------------------------------------------------------------
+/** @file SymmetricPoints.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "SymmetricPoints.h"
+
+#include "libboardgame_base/PointTransform.h"
+
+namespace libpentobi_base {
+
+using libboardgame_base::PointTransfRot180;
+
+//-----------------------------------------------------------------------------
+
+void SymmetricPoints::init(const Geometry& geo)
+{
+    PointTransfRot180<Point> 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 (file)
index 0000000..151452a
--- /dev/null
@@ -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<Point> 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 (file)
index 0000000..8d38446
--- /dev/null
@@ -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 <config.h>
+#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 (file)
index 0000000..34171d1
--- /dev/null
@@ -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 (file)
index 0000000..706980a
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "TrigonGeometry.h"
+
+namespace libpentobi_base {
+
+using libboardgame_base::CoordPoint;
+
+//-----------------------------------------------------------------------------
+
+map<unsigned, shared_ptr<TrigonGeometry>> 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<TrigonGeometry> 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 (file)
index 0000000..03118ad
--- /dev/null
@@ -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 <map>
+#include <memory>
+#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:
+    <tt>
+       0 1 2 3 4 5 6 7 8 9 10
+    0     / \ / \ / \ / \
+    1   / \ / \ / \ / \ / \
+    2 / \ / \ / \ / \ / \ / \
+    3 \ / \ / \ / \ / \ / \ /
+    4   \ / \ / \ / \ / \ /
+    5     \ / \ / \ / \ /
+    </tt>
+    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<unsigned, shared_ptr<TrigonGeometry>> 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 (file)
index 0000000..39a0675
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "TrigonTransform.h"
+
+#include <cmath>
+
+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<float>(p.x);
+    float py = static_cast<float>(p.y);
+    int x = static_cast<int>(ceil(0.5f * px - 1.5f * py));
+    int y = static_cast<int>(floor(0.5f * px + 0.5f * py));
+    return CoordPoint(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonRot120::get_transformed(const CoordPoint& p) const
+{
+    float px = static_cast<float>(p.x);
+    float py = static_cast<float>(p.y);
+    int x = static_cast<int>(ceil(-0.5f * px - 1.5f * py));
+    int y = static_cast<int>(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<float>(p.x);
+    float py = static_cast<float>(p.y);
+    int x = static_cast<int>(floor(-0.5f * px + 1.5f * py));
+    int y = static_cast<int>(ceil(-0.5f * px - 0.5f * py));
+    return CoordPoint(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonRot300::get_transformed(const CoordPoint& p) const
+{
+    float px = static_cast<float>(p.x);
+    float py = static_cast<float>(p.y);
+    int x = static_cast<int>(floor(0.5f * px + 1.5f * py));
+    int y = static_cast<int>(floor(-0.5f * px + 0.5f * py));
+    return CoordPoint(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonReflRot60::get_transformed(const CoordPoint& p) const
+{
+    float px = static_cast<float>(p.x);
+    float py = static_cast<float>(p.y);
+    int x = static_cast<int>(ceil(0.5f * (-px) - 1.5f * py));
+    int y = static_cast<int>(floor(0.5f * (-px) + 0.5f * py));
+    return CoordPoint(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonReflRot120::get_transformed(const CoordPoint& p) const
+{
+    float px = static_cast<float>(p.x);
+    float py = static_cast<float>(p.y);
+    int x = static_cast<int>(ceil(-0.5f * (-px) - 1.5f * py));
+    int y = static_cast<int>(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<float>(p.x);
+    float py = static_cast<float>(p.y);
+    int x = static_cast<int>(floor(-0.5f * (-px) + 1.5f * py));
+    int y = static_cast<int>(ceil(-0.5f * (-px) - 0.5f * py));
+    return CoordPoint(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonReflRot300::get_transformed(const CoordPoint& p) const
+{
+    float px = static_cast<float>(p.x);
+    float py = static_cast<float>(p.y);
+    int x = static_cast<int>(floor(0.5f * (-px) + 1.5f * py));
+    int y = static_cast<int>(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 (file)
index 0000000..4e33871
--- /dev/null
@@ -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 (file)
index 0000000..c0ee1f7
--- /dev/null
@@ -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 <config.h>
+#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<Point>::get(14, 14);
+        break;
+    case BoardType::classic:
+        result = &RectGeometry<Point>::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<unique_ptr<PointTransform<Point>>>& transforms,
+                    vector<unique_ptr<PointTransform<Point>>>& inv_transforms)
+{
+    transforms.clear();
+    inv_transforms.clear();
+    transforms.emplace_back(new PointTransfIdent<Point>);
+    inv_transforms.emplace_back(new PointTransfIdent<Point>);
+    switch (get_board_type(variant))
+    {
+    case BoardType::duo:
+        transforms.emplace_back(new PointTransfRot270Refl<Point>);
+        inv_transforms.emplace_back(new PointTransfRot270Refl<Point>);
+        break;
+    case BoardType::trigon:
+        transforms.emplace_back(new PointTransfTrigonRot60<Point>);
+        inv_transforms.emplace_back(new PointTransfTrigonRot300<Point>);
+        transforms.emplace_back(new PointTransfTrigonRot120<Point>);
+        inv_transforms.emplace_back(new PointTransfTrigonRot240<Point>);
+        transforms.emplace_back(new PointTransfRot180<Point>);
+        inv_transforms.emplace_back(new PointTransfRot180<Point>);
+        transforms.emplace_back(new PointTransfTrigonRot240<Point>);
+        inv_transforms.emplace_back(new PointTransfTrigonRot120<Point>);
+        transforms.emplace_back(new PointTransfTrigonRot300<Point>);
+        inv_transforms.emplace_back(new PointTransfTrigonRot60<Point>);
+        transforms.emplace_back(new PointTransfRefl<Point>);
+        inv_transforms.emplace_back(new PointTransfRefl<Point>);
+        transforms.emplace_back(new PointTransfTrigonReflRot60<Point>);
+        inv_transforms.emplace_back(new PointTransfTrigonReflRot60<Point>);
+        transforms.emplace_back(new PointTransfTrigonReflRot120<Point>);
+        inv_transforms.emplace_back(new PointTransfTrigonReflRot120<Point>);
+        transforms.emplace_back(new PointTransfReflRot180<Point>);
+        inv_transforms.emplace_back(new PointTransfReflRot180<Point>);
+        transforms.emplace_back(new PointTransfTrigonReflRot240<Point>);
+        inv_transforms.emplace_back(new PointTransfTrigonReflRot240<Point>);
+        transforms.emplace_back(new PointTransfTrigonReflRot300<Point>);
+        inv_transforms.emplace_back(new PointTransfTrigonReflRot300<Point>);
+        break;
+    case BoardType::callisto_2:
+    case BoardType::callisto:
+    case BoardType::callisto_3:
+        transforms.emplace_back(new PointTransfRot90<Point>);
+        inv_transforms.emplace_back(new PointTransfRot270<Point>);
+        transforms.emplace_back(new PointTransfRot180<Point>);
+        inv_transforms.emplace_back(new PointTransfRot180<Point>);
+        transforms.emplace_back(new PointTransfRot270<Point>);
+        inv_transforms.emplace_back(new PointTransfRot90<Point>);
+        transforms.emplace_back(new PointTransfRefl<Point>);
+        inv_transforms.emplace_back(new PointTransfRefl<Point>);
+        transforms.emplace_back(new PointTransfReflRot180<Point>);
+        inv_transforms.emplace_back(new PointTransfReflRot180<Point>);
+        transforms.emplace_back(new PointTransfRot90Refl<Point>);
+        inv_transforms.emplace_back(new PointTransfRot90Refl<Point>);
+        transforms.emplace_back(new PointTransfRot270Refl<Point>);
+        inv_transforms.emplace_back(new PointTransfRot270Refl<Point>);
+        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 (file)
index 0000000..dcf8a94
--- /dev/null
@@ -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 <memory>
+#include <string>
+#include <vector>
+#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<unique_ptr<PointTransform<Point>>>& transforms,
+                    vector<unique_ptr<PointTransform<Point>>>& 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 (file)
index 0000000..2c8c6de
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "BoardPainter.h"
+#include "libpentobi_base/CallistoGeometry.h"
+
+#include <algorithm>
+#include <cmath>
+#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<int>((x - m_boardOffset.x()) / m_fieldWidth);
+    y = static_cast<int>((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<int>(m_geo->get_width());
+    m_height = static_cast<int>(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<int>((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>& pointState,
+                                 const Grid<unsigned>& pieceId, int x, int y,
+                                 qreal fieldX, qreal fieldY)
+{
+    LIBBOARDGAME_ASSERT(m_geo->get_point_type(x, y) == 0);
+    ArrayList<unsigned, 4> 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>& pointState,
+                               Variant variant, const Grid<QString>& 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>& pointState,
+                              Variant variant, const Grid<int>& marks)
+{
+    for (Point p : *m_geo)
+        if (marks[p] & (dot | circle))
+        {
+            qreal x = (static_cast<float>(m_geo->get_x(p)) + 0.5f)
+                    * m_fieldWidth;
+            qreal y = (static_cast<float>(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>& pointState,
+                               const Grid<unsigned>& pieceId,
+                               const Grid<QString>* labels,
+                               const Grid<int>* marks)
+{
+    painter.setRenderHint(QPainter::Antialiasing, true);
+    painter.save();
+    painter.translate(m_boardOffset);
+    ColorMap<bool> 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<Point, 2 * PieceInfo::max_size> 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>& pointState,
+                                       const ColorMap<bool>& 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 (file)
index 0000000..560fa67
--- /dev/null
@@ -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 <QPainter>
+#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>& pointState,
+                     const Grid<unsigned>& pieceId,
+                     const Grid<QString>* labels = nullptr,
+                     const Grid<int>* 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>& pointState,
+                       const Grid<unsigned>& 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>& pointState,
+                     Variant variant, const Grid<QString>& labels);
+
+    void paintMarks(QPainter& painter, const Grid<PointState>& pointState,
+                    Variant variant, const Grid<int>& marks);
+
+    void paintStartingPoints(QPainter& painter, Variant variant,
+                             const Grid<PointState>& pointState,
+                             const ColorMap<bool>& 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 (file)
index 0000000..9283f38
--- /dev/null
@@ -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 (file)
index 0000000..ec354fb
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "ComputerColorDialog.h"
+
+#include <QDialogButtonBox>
+#include <QLabel>
+#include <QVBoxLayout>
+
+//-----------------------------------------------------------------------------
+
+ComputerColorDialog::ComputerColorDialog(QWidget* parent,
+                                         Variant variant,
+                                         ColorMap<bool>& 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 (file)
index 0000000..328a366
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include <array>
+#include <QCheckBox>
+#include <QDialog>
+#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<bool>& computerColor);
+
+public slots:
+    void accept() override;
+
+private:
+    ColorMap<bool>& m_computerColor;
+
+    Variant m_variant;
+
+    array<QCheckBox*, 4> 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 (file)
index 0000000..06bad4b
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "GameInfoDialog.h"
+
+#include <QDialogButtonBox>
+#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 (file)
index 0000000..f5a9c65
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include <QDialog>
+#include <QFormLayout>
+#include <QLineEdit>
+#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 (file)
index 0000000..6c0e525
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "GuiBoard.h"
+
+#include <QApplication>
+#include <QMouseEvent>
+#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<int>(geo.get_width());
+    int height = static_cast<int>(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<int>(geo.get_width());
+        int height = static_cast<int>(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 (file)
index 0000000..dc48ef3
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include <memory>
+#include <QTimer>
+#include <QWidget>
+#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<QString>& 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<unsigned> m_pieceId;
+
+    Piece m_selectedPiece = Piece::null();
+
+    Color m_selectedPieceColor;
+
+    const Transform* m_selectedPieceTransform = nullptr;
+
+    CoordPoint m_selectedPieceOffset;
+
+    MovePoints m_selectedPiecePoints;
+
+    Grid<QString> m_labels;
+
+    Grid<int> m_marks;
+
+    BoardPainter m_boardPainter;
+
+    unique_ptr<QPixmap> m_emptyBoardPixmap;
+
+    unique_ptr<QPixmap> 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<QString>& 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 (file)
index 0000000..d560eeb
--- /dev/null
@@ -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 <config.h>
+#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 (file)
index 0000000..951427b
--- /dev/null
@@ -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 (file)
index 0000000..0848c33
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "HelpWindow.h"
+
+#include <QApplication>
+#include <QAction>
+#include <QDesktopWidget>
+#include <QFile>
+#include <QLocale>
+#include <QSettings>
+#include <QTextBrowser>
+#include <QToolBar>
+#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 (file)
index 0000000..d26328c
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include <QMainWindow>
+#include <QUrl>
+
+//-----------------------------------------------------------------------------
+
+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 (file)
index 0000000..80144a8
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "InitialRatingDialog.h"
+
+#include <QDialogButtonBox>
+#include <QLabel>
+#include <QSlider>
+#include <QVBoxLayout>
+
+//-----------------------------------------------------------------------------
+
+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 (file)
index 0000000..651370c
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include <QDialog>
+
+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 (file)
index 0000000..2f96636
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "LeaveFullscreenButton.h"
+
+#include <QApplication>
+#include <QDesktopWidget>
+#include <QPropertyAnimation>
+#include <QTimer>
+#include <QToolButton>
+
+//-----------------------------------------------------------------------------
+
+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 (file)
index 0000000..19d85cd
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include <QObject>
+#include <QPoint>
+
+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 (file)
index 0000000..34d6d85
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "LineEdit.h"
+
+#include <QApplication>
+
+//-----------------------------------------------------------------------------
+
+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 (file)
index 0000000..3dbf421
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include <QLineEdit>
+
+//-----------------------------------------------------------------------------
+
+/** 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 (file)
index 0000000..19dcfbd
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "OrientationDisplay.h"
+
+#include <QPainter>
+#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<CoordPoint, 2 * PieceInfo::max_size> 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 (file)
index 0000000..ba28eda
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include <QWidget>
+#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 (file)
index 0000000..d6d3029
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "PieceSelector.h"
+
+#include <QMouseEvent>
+#include <QPainter>
+#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<int>(pixelX / m_fieldWidth);
+    int y = static_cast<int>(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<unsigned> 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 (file)
index 0000000..2f49ebc
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include <string>
+#include <QWidget>
+#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 (file)
index 0000000..b61d887
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "SameHeightLayout.h"
+
+#include <QStyle>
+#include <QWidget>
+
+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 (file)
index 0000000..e2bf5ff
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include <QLayout>
+
+//-----------------------------------------------------------------------------
+
+/** 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<QLayoutItem*> 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 (file)
index 0000000..e6208f4
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "ScoreDisplay.h"
+
+#include <cmath>
+#include <QApplication>
+#include <QPainter>
+#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<int>(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<int>(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<int>(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<int>(0.8 * m_fontSize);
+    m_colorDotSpace = static_cast<int>(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<int>(x));
+        x += m_colorDotWidth + textWidthBlue + pad;
+        drawScore(painter, Color(1), static_cast<int>(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<int>(x));
+        x += m_colorDotWidth + textWidthBlue + pad;
+        drawScore(painter, Color(1), static_cast<int>(x));
+        x += m_colorDotWidth + textWidthYellow + pad;
+        drawScore(painter, Color(2), static_cast<int>(x));
+        x += m_colorDotWidth + textWidthRed + pad;
+        drawScore(painter, Color(3), static_cast<int>(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<int>(x));
+        x += m_colorDotWidth + textWidthBlue + pad;
+        drawScore(painter, Color(1), static_cast<int>(x));
+        x += m_colorDotWidth + textWidthYellow + pad;
+        drawScore(painter, Color(2), static_cast<int>(x));
+        x += m_colorDotWidth + textWidthRed + pad;
+        drawScore3(painter, static_cast<int>(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<int>(x));
+        x += m_colorDotWidth + textWidthBlue + pad;
+        drawScore(painter, Color(1), static_cast<int>(x));
+        x += m_colorDotWidth + textWidthYellow + pad;
+        drawScore(painter, Color(2), static_cast<int>(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<int>(x));
+        x += m_twoColorDotWidth + textWidthBlueRed + pad;
+        drawScore2(painter, Color(1), Color(3), static_cast<int>(x));
+        x += m_twoColorDotWidth + textWidthYellowGreen + pad;
+        drawScore(painter, Color(0), static_cast<int>(x));
+        x += m_colorDotWidth + textWidthBlue + pad;
+        drawScore(painter, Color(1), static_cast<int>(x));
+        x += m_colorDotWidth + textWidthYellow + pad;
+        drawScore(painter, Color(2), static_cast<int>(x));
+        x += m_colorDotWidth + textWidthRed + pad;
+        drawScore(painter, Color(3), static_cast<int>(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<int>(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 (file)
index 0000000..19e2861
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include <QWidget>
+#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<bool> m_hasMoves{false};
+
+    ColorMap<ScoreType> m_points{0};
+
+    ColorMap<ScoreType> 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 (file)
index 0000000..aad3a2f
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "Util.h"
+
+#include <QCoreApplication>
+
+//-----------------------------------------------------------------------------
+
+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<int>(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 (file)
index 0000000..4a070a3
--- /dev/null
@@ -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 <QColor>
+#include <QPainter>
+#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 (file)
index 0000000..61e1d58
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <g stroke-linejoin="round" stroke="#2e3436">
+  <path d="m11 2-7.5 8.5v9h15v-9z" fill="#babdb6"/>
+  <path d="m11 1.5-10 10 1.53 1.34 8.47-8.34 8.5 8.38 1.5-1.38z" fill="#babdb6"/>
+  <rect x="8.5" y="12.5" width="5" height="7" fill="none"/>
+ </g>
+</svg>
diff --git a/src/libpentobi_gui/icons/go-next.svg b/src/libpentobi_gui/icons/go-next.svg
new file mode 100644 (file)
index 0000000..0d797a9
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <path stroke-linejoin="round" d="m11.5 2.5v4h-10v9h10v4l8.5-8.5z" stroke="#2e3436" fill="#babdb6"/>
+</svg>
diff --git a/src/libpentobi_gui/icons/go-previous.svg b/src/libpentobi_gui/icons/go-previous.svg
new file mode 100644 (file)
index 0000000..0aee1b4
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <path stroke-linejoin="round" d="m10.5 2.5v4h10v9h-10v4l-8.5-8.5z" stroke="#2e3436" fill="#babdb6"/>
+</svg>
diff --git a/src/libpentobi_gui/libpentobi_gui_resources.qrc b/src/libpentobi_gui/libpentobi_gui_resources.qrc
new file mode 100644 (file)
index 0000000..0206df4
--- /dev/null
@@ -0,0 +1,8 @@
+<!DOCTYPE RCC>
+<RCC version="1.0">
+  <qresource prefix="/libpentobi_gui">
+    <file>icons/go-home.png</file>
+    <file>icons/go-next.png</file>
+    <file>icons/go-previous.png</file>
+  </qresource>
+</RCC>
diff --git a/src/libpentobi_gui/libpentobi_gui_resources_2x.qrc b/src/libpentobi_gui/libpentobi_gui_resources_2x.qrc
new file mode 100644 (file)
index 0000000..e81e663
--- /dev/null
@@ -0,0 +1,8 @@
+<!DOCTYPE RCC>
+<RCC version="1.0">
+  <qresource prefix="/libpentobi_gui">
+    <file>icons/go-home@2x.png</file>
+    <file>icons/go-next@2x.png</file>
+    <file>icons/go-previous@2x.png</file>
+  </qresource>
+</RCC>
diff --git a/src/libpentobi_gui/translations/libpentobi_gui_de.ts b/src/libpentobi_gui/translations/libpentobi_gui_de.ts
new file mode 100644 (file)
index 0000000..cb3ef0b
--- /dev/null
@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="de_DE">
+<context>
+    <name>ComputerColorDialog</name>
+    <message>
+        <source>Computer Colors</source>
+        <translation>Computer-Farben</translation>
+    </message>
+    <message>
+        <source>Computer plays:</source>
+        <translation>Computer spielt:</translation>
+    </message>
+    <message>
+        <source>&amp;Blue</source>
+        <translation>&amp;Blau</translation>
+    </message>
+    <message>
+        <source>&amp;Green</source>
+        <translation>&amp;Grün</translation>
+    </message>
+    <message>
+        <source>&amp;Yellow</source>
+        <translation>G&amp;elb</translation>
+    </message>
+    <message>
+        <source>&amp;Red</source>
+        <translation>&amp;Rot</translation>
+    </message>
+    <message>
+        <source>&amp;Blue/Red</source>
+        <translation>&amp;Blau/Rot</translation>
+    </message>
+    <message>
+        <source>&amp;Yellow/Green</source>
+        <translation>&amp;Gelb/Grün</translation>
+    </message>
+</context>
+<context>
+    <name>GameInfoDialog</name>
+    <message>
+        <source>Game Info</source>
+        <translation>Spielinformation</translation>
+    </message>
+    <message>
+        <source>Player &amp;Blue:</source>
+        <oldsource>Player Blue:</oldsource>
+        <translation>Spieler &amp;Blau:</translation>
+    </message>
+    <message>
+        <source>Player &amp;Green:</source>
+        <oldsource>Player Green:</oldsource>
+        <translation>Spieler &amp;Grün:</translation>
+    </message>
+    <message>
+        <source>Player &amp;Yellow:</source>
+        <oldsource>Player Yellow:</oldsource>
+        <translation>Spieler G&amp;elb:</translation>
+    </message>
+    <message>
+        <source>Player &amp;Red:</source>
+        <oldsource>Player Red:</oldsource>
+        <translation>Spieler &amp;Rot:</translation>
+    </message>
+    <message>
+        <source>Player &amp;Blue/Red:</source>
+        <oldsource>Player Blue/Red:</oldsource>
+        <translation>Spieler &amp;Blau/Rot:</translation>
+    </message>
+    <message>
+        <source>Player &amp;Yellow/Green:</source>
+        <oldsource>Player Yellow/Green:</oldsource>
+        <translation>Spieler &amp;Gelb/Grün:</translation>
+    </message>
+    <message>
+        <source>&amp;Date:</source>
+        <oldsource>Date:</oldsource>
+        <translation>&amp;Datum:</translation>
+    </message>
+    <message>
+        <source>&amp;Time limits:</source>
+        <translation>Bedenk&amp;zeit:</translation>
+    </message>
+    <message>
+        <source>&amp;Event:</source>
+        <translation>&amp;Veranstaltung:</translation>
+    </message>
+    <message>
+        <source>R&amp;ound:</source>
+        <translation>R&amp;unde:</translation>
+    </message>
+</context>
+<context>
+    <name>HelpWindow</name>
+    <message>
+        <source>Back</source>
+        <translation>Zurück</translation>
+    </message>
+    <message>
+        <source>Show previous page in history</source>
+        <translation>Die vorherige Seite in der Chronik anzeigen</translation>
+    </message>
+    <message>
+        <source>Forward</source>
+        <translation>Vorwärts</translation>
+    </message>
+    <message>
+        <source>Show next page in history</source>
+        <translation>Die nächste Seite in der Chronik anzeigen</translation>
+    </message>
+    <message>
+        <source>Contents</source>
+        <translation>Inhalt</translation>
+    </message>
+    <message>
+        <source>Show table of contents</source>
+        <translation>Das Inhaltsverzeichnis anzeigen</translation>
+    </message>
+</context>
+<context>
+    <name>InitialRatingDialog</name>
+    <message>
+        <source>Initial Rating</source>
+        <translation>Anfangswertung</translation>
+    </message>
+    <message>
+        <source>You have not yet played rated games in this game variant. Estimate your playing strength to initialize your rating.</source>
+        <translation>Sie haben noch keine gewerteten Spiele in dieser Spielvariante gespielt. Schätzen Sie Ihre Spielstärke, um Ihre Wertung zu initialisieren.</translation>
+    </message>
+    <message>
+        <source>Beginner</source>
+        <translation>Anfänger</translation>
+    </message>
+    <message>
+        <source>Expert</source>
+        <translation>Experte</translation>
+    </message>
+    <message>
+        <source>Your initial rating: %1</source>
+        <translation>Ihre Anfangswertung: %1</translation>
+    </message>
+</context>
+<context>
+    <name>Util</name>
+    <message>
+        <source>Blue</source>
+        <translation>Blau</translation>
+    </message>
+    <message>
+        <source>Green</source>
+        <translation>Grün</translation>
+    </message>
+    <message>
+        <source>Yellow</source>
+        <translation>Gelb</translation>
+    </message>
+    <message>
+        <source>Red</source>
+        <translation>Rot</translation>
+    </message>
+    <message>
+        <source>Blue/Red</source>
+        <translation>Blau/Rot</translation>
+    </message>
+    <message>
+        <source>Yellow/Green</source>
+        <translation>Gelb/Grün</translation>
+    </message>
+</context>
+</TS>
diff --git a/src/libpentobi_kde_thumbnailer/CMakeLists.txt b/src/libpentobi_kde_thumbnailer/CMakeLists.txt
new file mode 100644 (file)
index 0000000..a721e3d
--- /dev/null
@@ -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 (file)
index 0000000..f4d68b9
--- /dev/null
@@ -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 <config.h>
+#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<void(unsigned,unsigned)> progress_callback)
+{
+    m_variant = game.get_variant();
+    m_moves.clear();
+    m_values.clear();
+    auto& tree = game.get_tree();
+    unique_ptr<Board> 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 (file)
index 0000000..3b7e601
--- /dev/null
@@ -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 <functional>
+#include <vector>
+#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<void(unsigned,unsigned)> 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<ColorMove> m_moves;
+
+    vector<double> 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<unsigned>(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 (file)
index 0000000..9acef96
--- /dev/null
@@ -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 (file)
index 0000000..10ece26
--- /dev/null
@@ -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 <type_traits>
+
+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<Float>::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 (file)
index 0000000..290df35
--- /dev/null
@@ -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 <config.h>
+#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<Board> 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<Move, SearchParamConst::max_moves>& 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 (file)
index 0000000..8d9f145
--- /dev/null
@@ -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<Move, SearchParamConst::max_moves>& 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<ColorMove, Board::max_game_moves> 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 (file)
index 0000000..5bd0a69
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "Player.h"
+
+#include <fstream>
+#include <iomanip>
+#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<float>(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<size_t>(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 (file)
index 0000000..3aeb8ce
--- /dev/null
@@ -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<float, Board::max_player_moves> m_weight_max_count_classic;
+
+    array<float, Board::max_player_moves> m_weight_max_count_trigon;
+
+    array<float, Board::max_player_moves> m_weight_max_count_duo;
+
+    array<float, Board::max_player_moves> m_weight_max_count_callisto;
+
+    array<float, Board::max_player_moves> 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<TimeSource> 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 (file)
index 0000000..54fe564
--- /dev/null
@@ -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 <config.h>
+#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 (file)
index 0000000..a3a535f
--- /dev/null
@@ -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<unsigned MAX_SIZE>
+    void set_forbidden(const MoveInfo<MAX_SIZE>& info);
+
+    /** Set adjacent points of move to forbidden. */
+    template<unsigned MAX_ADJ_ATTACH>
+    void set_forbidden(const MoveInfoExt<MAX_ADJ_ATTACH>& info_ext);
+
+    template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+    void set_local(const Board& bd);
+
+private:
+    GridExt<IntType> m_point_value;
+
+    Grid<IntType> 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<unsigned MAX_SIZE>
+inline void PlayoutFeatures::set_forbidden(const MoveInfo<MAX_SIZE>& 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<unsigned MAX_ADJ_ATTACH>
+inline void PlayoutFeatures::set_forbidden(
+        const MoveInfoExt<MAX_ADJ_ATTACH>& info_ext)
+{
+    for (auto i = info_ext.begin_adj(), end = info_ext.end_adj(); i != end;
+         ++i)
+        m_point_value[*i] = 0x1000u;
+}
+
+template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+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<MAX_ADJ_ATTACH>(
+                    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<IntType>(
+                            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<IntType>(
+                                    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 (file)
index 0000000..369347e
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "PriorKnowledge.h"
+
+#include <cmath>
+#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<float>(geo.get_width());
+    float height = static_cast<float>(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<float>(geo.get_x(p));
+        float y = static_cast<float>(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<float>::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 (file)
index 0000000..6bfae56
--- /dev/null
@@ -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<Move, Float, SearchParamConst::multithread>
+    Node;
+
+    typedef libboardgame_mcts::Tree<Node> 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<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+    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<MoveFeatures, Move::range> m_features;
+
+    /** Maximum of Features::heuristic for all moves. */
+    Float m_max_heuristic;
+
+    bool m_has_connect_move;
+
+    ColorMap<bool> 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<bool> m_is_local;
+
+    /** Points in m_is_local with value greater zero. */
+    PointList m_local_points;
+
+    /** Distance to center heuristic. */
+    GridExt<float> m_dist_to_center;
+
+
+    template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+    void compute_features(const Board& bd, const MoveList& moves,
+                          bool check_dist_to_center, bool check_connect);
+
+    template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+    void init_local(const Board& bd);
+};
+
+
+template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+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<Float> point_value;
+    point_value[Point::null()] = 0;
+    Grid<Float> attach_point_value;
+    Grid<Float> 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<Float>::max();
+    m_min_dist_to_center = numeric_limits<unsigned short>::max();
+    m_has_connect_move = false;
+    for (unsigned i = 0; i < moves.size(); ++i)
+    {
+        auto mv = moves[i];
+        auto info = BoardConst::get_move_info<MAX_SIZE>(mv, move_info_array);
+        auto& info_ext = BoardConst::get_move_info_ext<MAX_ADJ_ATTACH>(
+                    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<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+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<MAX_SIZE, MAX_ADJ_ATTACH>(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<MAX_SIZE, MAX_ADJ_ATTACH>(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<unsigned short>(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<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+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<MAX_ADJ_ATTACH>(
+                    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 (file)
index 0000000..26e1e88
--- /dev/null
@@ -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 <config.h>
+#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<Move, max_moves>& 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<State> Search::create_state()
+{
+    return unique_ptr<State>(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 (file)
index 0000000..e1d41cb
--- /dev/null
@@ -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<State, Move, SearchParamConst>
+{
+public:
+    Search(Variant initial_variant, unsigned nu_threads, size_t memory);
+
+    ~Search();
+
+    unique_ptr<State> create_state() override;
+
+    PlayerInt get_nu_players() const override;
+
+    PlayerInt get_player() const override;
+
+    bool check_followup(ArrayList<Move, max_moves>& 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<PlayerInt>(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 (file)
index 0000000..aaa8558
--- /dev/null
@@ -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 (file)
index 0000000..906d7da
--- /dev/null
@@ -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 <config.h>
+#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<bool>& 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<bool>& 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<bool>& 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<bool> 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 (file)
index 0000000..a02d776
--- /dev/null
@@ -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<PrecompMoves> 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<const PieceMap<bool>*, Board::max_game_moves> is_piece_considered;
+
+    /** List of unique values for is_piece_considered. */
+    ArrayList<PieceMap<bool>, 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<bool> is_piece_considered_all;
+
+    PieceMap<bool> 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<Move, Point::max_onboard> 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 (file)
index 0000000..5b3da1c
--- /dev/null
@@ -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 <config.h>
+#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<unsigned MAX_SIZE, bool IS_CALLISTO>
+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<MAX_SIZE, IS_CALLISTO>(
+                           mv, get_move_info<MAX_SIZE>(mv), gamma_piece, moves,
+                           nu_moves, playout_features, total_gamma))
+                marker.set(mv);
+    }
+}
+
+template<unsigned MAX_SIZE>
+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<unsigned MAX_SIZE>
+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<MAX_SIZE>(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<unsigned MAX_SIZE>
+bool State::check_forbidden(const GridExt<bool>& is_forbidden, Move mv,
+                            MoveList& moves, unsigned& nu_moves)
+{
+    auto p = get_move_info<MAX_SIZE>(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<unsigned>(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<unsigned MAX_SIZE, bool IS_CALLISTO>
+bool State::check_move(Move mv, const MoveInfo<MAX_SIZE>& 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<unsigned MAX_SIZE, bool IS_CALLISTO>
+inline bool State::check_move(Move mv, const MoveInfo<MAX_SIZE>& info,
+                              MoveList& moves, unsigned& nu_moves,
+                              const PlayoutFeatures& playout_features,
+                              float& total_gamma)
+{
+    return check_move<MAX_SIZE, IS_CALLISTO>(
+                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<Float, 6>& 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<Float, 6>& result)
+{
+    auto nu_players = m_bd.get_nu_players();
+    LIBBOARDGAME_ASSERT(nu_players > 2);
+    array<ScoreType, Color::range> points;
+    for (Color::IntType i = 0; i < nu_players; ++i)
+        points[i] = m_bd.get_points(Color(i));
+    array<Float, Color::range> 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<Float, 6>& 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<float>(geo.get_x(p));
+        float py = static_cast<float>(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<float>(geo.get_x(pp));
+                    float ppy = static_cast<float>(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<Move>& 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<Move>(get_player(),
+                          moves[static_cast<unsigned>(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<bool>& 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<Float>(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<Float>(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<Float>(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<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH, bool IS_CALLISTO>
+void State::init_moves_with_gamma(Color c)
+{
+    m_is_piece_considered[c] = &get_is_piece_considered(c);
+    m_playout_features[c].set_local<MAX_SIZE, MAX_ADJ_ATTACH>(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<MAX_SIZE>(c, pieces, true, moves);
+    else
+    {
+        unsigned nu_moves = 0;
+        float total_gamma = 0;
+        if (MAX_SIZE == 5 && m_is_callisto)
+            add_one_piece_moves<MAX_SIZE>(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<MAX_SIZE, IS_CALLISTO>(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<MAX_SIZE, MAX_ADJ_ATTACH, IS_CALLISTO>(c);
+    }
+}
+
+template<unsigned MAX_SIZE>
+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<MAX_SIZE>(c, pieces, false, moves);
+    else
+    {
+        unsigned nu_moves = 0;
+        if (MAX_SIZE == 5 && m_is_callisto)
+        {
+            float total_gamma_dummy;
+            add_one_piece_moves<MAX_SIZE>(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<MAX_SIZE>(
+                                    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<MAX_SIZE>(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<float>(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<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH, bool IS_CALLISTO>
+void State::update_moves(Color c)
+{
+    auto& playout_features = m_playout_features[c];
+    playout_features.set_local<MAX_SIZE, MAX_ADJ_ATTACH>(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<MAX_SIZE>(m_last_move[c]).get_piece())))
+        for (Move mv : moves)
+        {
+            auto& info = get_move_info<MAX_SIZE>(mv);
+            if (info.get_piece() == piece
+                    || ! check_move<MAX_SIZE, IS_CALLISTO>(
+                             mv, info, moves, nu_moves, playout_features,
+                             total_gamma))
+                marker.clear(mv);
+        }
+    else
+        for (Move mv : moves)
+        {
+            auto& info = get_move_info<MAX_SIZE>(mv);
+            if (! m_bd.is_piece_left(c, info.get_piece())
+                    || ! check_move<MAX_SIZE, IS_CALLISTO>(
+                             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<MAX_SIZE, IS_CALLISTO>(*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<MAX_SIZE, IS_CALLISTO>(
+                        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 (file)
index 0000000..84fd37a
--- /dev/null
@@ -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<Move, Float, SearchParamConst::multithread>
+        Node;
+
+    typedef libboardgame_mcts::Tree<Node> Tree;
+
+    typedef libboardgame_mcts::LastGoodReply<Move,
+                                             SearchParamConst::max_players,
+                                             SearchParamConst::lgr_hash_table_size,
+                                             SearchParamConst::multithread>
+        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>& move);
+
+    void evaluate_playout(array<Float, 6>& 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<float, MoveList::max_size> 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<MoveList> m_moves;
+
+    ColorMap<const PieceMap<bool>*> 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<float> m_gamma_piece;
+
+    /** Number of moves played by a color since the last update of its move
+        list. */
+    ColorMap<unsigned> m_nu_new_moves;
+
+    /** Board::get_attach_points().end() for a color at the last update of
+        its move list. */
+    ColorMap<PointList::const_iterator> m_last_attach_points_end;
+
+    /** Last move played by a color since the last update of its move list. */
+    ColorMap<Move> m_last_move;
+
+    ColorMap<bool> m_is_move_list_initialized;
+
+    ColorMap<bool> m_has_moves;
+
+    /** Marks moves contained in m_moves. */
+    ColorMap<MoveMarker> m_marker;
+
+    ColorMap<PlayoutFeatures> m_playout_features;
+
+    RandomGenerator m_random;
+
+    /** Used in get_quality_bonus(). */
+    ColorMap<Statistics<Float>> m_stat_score;
+
+    /** Used in get_quality_bonus(). */
+    Statistics<Float> m_stat_len;
+
+    /** Used in get_quality_bonus(). */
+    Statistics<Float> 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<Grid<bool>> m_moves_added_at;
+
+
+    template<unsigned MAX_SIZE, bool IS_CALLISTO>
+    void add_moves(Point p, Color c, const Board::PiecesLeftList& pieces,
+                   float& total_gamma, MoveList& moves, unsigned& nu_moves);
+
+    template<unsigned MAX_SIZE>
+    LIBBOARDGAME_NOINLINE
+    void add_starting_moves(Color c, const Board::PiecesLeftList& pieces,
+                            bool with_gamma, MoveList& moves);
+
+    template<unsigned MAX_SIZE>
+    LIBBOARDGAME_NOINLINE
+    void add_one_piece_moves(Color c, bool with_gamma, float& total_gamma,
+                             MoveList& moves, unsigned& nu_moves);
+
+    void evaluate_multicolor(array<Float, 6>& result);
+
+    void evaluate_multiplayer(array<Float, 6>& result);
+
+    void evaluate_twocolor(array<Float, 6>& 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<unsigned MAX_SIZE>
+    const MoveInfo<MAX_SIZE>& get_move_info(Move mv) const;
+
+    template<unsigned MAX_ADJ_ATTACH>
+    const MoveInfoExt<MAX_ADJ_ATTACH>& 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<bool>& get_is_piece_considered(Color c) const;
+
+    const Board::PiecesLeftList& get_pieces_considered(Color c);
+
+    template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH, bool IS_CALLISTO>
+    void init_moves_with_gamma(Color c);
+
+    template<unsigned MAX_SIZE>
+    void init_moves_without_gamma(Color c);
+
+    template<unsigned MAX_SIZE>
+    bool check_forbidden(const GridExt<bool>& is_forbidden, Move mv,
+                         MoveList& moves, unsigned& nu_moves);
+
+    bool check_lgr(Move mv) const;
+
+    template<unsigned MAX_SIZE, bool IS_CALLISTO>
+    bool check_move(Move mv, const MoveInfo<MAX_SIZE>& info, float gamma_piece,
+                    MoveList& moves, unsigned& nu_moves,
+                    const PlayoutFeatures& playout_features,
+                    float& total_gamma);
+
+    template<unsigned MAX_SIZE, bool IS_CALLISTO>
+    bool check_move(Move mv, const MoveInfo<MAX_SIZE>& info, MoveList& moves,
+                    unsigned& nu_moves,
+                    const PlayoutFeatures& playout_features,
+                    float& total_gamma);
+
+    bool gen_playout_move_full(PlayerMove<Move>& mv);
+
+    template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH, bool IS_CALLISTO>
+    void update_moves(Color c);
+
+    template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+    void update_playout_features(Color c, Move mv);
+
+    template<unsigned MAX_SIZE>
+    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<Float, 6>& 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<Move>& 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<Move>(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<Move>(player, lgr1);
+        return true;
+    }
+    return gen_playout_move_full(mv);
+}
+
+template<unsigned MAX_SIZE>
+inline const MoveInfo<MAX_SIZE>& State::get_move_info(Move mv) const
+{
+    LIBBOARDGAME_ASSERT(mv.to_int() < m_bc->get_nu_moves());
+    return BoardConst::get_move_info<MAX_SIZE>(mv, m_move_info_array);
+}
+
+template<unsigned MAX_ADJ_ATTACH>
+inline const MoveInfoExt<MAX_ADJ_ATTACH>& State::get_move_info_ext(
+        Move mv) const
+{
+    LIBBOARDGAME_ASSERT(mv.to_int() < m_bc->get_nu_moves());
+    return BoardConst::get_move_info_ext<MAX_ADJ_ATTACH>(
+                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<PlayerInt>(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<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+inline void State::update_playout_features(Color c, Move mv)
+{
+    auto& info = get_move_info<MAX_SIZE>(mv);
+    for (Color i : Color::Range(m_nu_colors))
+        m_playout_features[i].set_forbidden(info);
+    m_playout_features[c].set_forbidden<MAX_ADJ_ATTACH>(
+                get_move_info_ext<MAX_ADJ_ATTACH>(mv));
+}
+
+template<unsigned MAX_SIZE>
+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<MAX_SIZE>(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 (file)
index 0000000..c85c2f5
--- /dev/null
@@ -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 <config.h>
+#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<Color, Color::range> 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<Point::IntType>(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 (file)
index 0000000..9431948
--- /dev/null
@@ -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 (file)
index 0000000..0430c75
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "Util.h"
+
+#include <thread>
+#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<const Search::Node*> 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 (file)
index 0000000..eb96978
--- /dev/null
@@ -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 (file)
index 0000000..77acc34
--- /dev/null
@@ -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 (file)
index 0000000..3aef69b
--- /dev/null
@@ -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 <iostream>
+#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>& pointState,
+                 Grid<unsigned>& pieceId, unsigned& currentPieceId)
+{
+    vector<string> values = node.get_multi_property(id);
+    for (const string& s : values)
+    {
+        if (trim(s).empty())
+            continue;
+        vector<string> 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>& pointState, Grid<unsigned>& pieceId)
+{
+    vector<string> values = node.get_multi_property("AE");
+    for (const auto& s : values)
+    {
+        if (trim(s).empty())
+            continue;
+        vector<string> 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>& pointState,
+                      Grid<unsigned>& 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> pointState;
+    Grid<unsigned> 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 (file)
index 0000000..f2b1e50
--- /dev/null
@@ -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 (file)
index 0000000..0e762fb
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "AnalyzeGameWidget.h"
+
+#include <QApplication>
+#include <QDesktopWidget>
+#include <QLabel>
+#include <QMouseEvent>
+#include <QProgressDialog>
+#include <QtConcurrentRun>
+#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<unsigned>((event->x() - m_borderX) / m_dX);
+    if (moveNumber >= m_analyzeGame.get_nu_moves())
+        return;
+    vector<ColorMove> 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<int>(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<unsigned>(m_currentPosition) < nu_moves)
+    {
+        QPen pen(QColor(96, 96, 96));
+        pen.setStyle(Qt::DotLine);
+        painter.setPen(pen);
+        int x = static_cast<int>(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<ColorMove,Board::max_game_moves> 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 (file)
index 0000000..a549a26
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include <vector>
+#include <QFuture>
+#include <QWidget>
+#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<ColorMove>& 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<void> 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 (file)
index 0000000..5f4a238
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "AnalyzeGameWindow.h"
+
+#include <QVBoxLayout>
+#include <QDialogButtonBox>
+
+//-----------------------------------------------------------------------------
+
+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 (file)
index 0000000..d546fca
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include <QDialog>
+#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 (file)
index 0000000..3251b68
--- /dev/null
@@ -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 <config.h>
+#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 (file)
index 0000000..5bd73b2
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include <QInputDialog>
+
+//-----------------------------------------------------------------------------
+
+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 (file)
index 0000000..176adf4
--- /dev/null
@@ -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 <config.h>
+#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 (file)
index 0000000..8e497fe
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include <QApplication>
+
+//-----------------------------------------------------------------------------
+
+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 (file)
index 0000000..ecae6de
--- /dev/null
@@ -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 (file)
index 0000000..a6b0f92
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include <QCommandLineParser>
+#include <QFileInfo>
+#include <QLibraryInfo>
+#include <QMessageBox>
+#include <QSettings>
+#include <QStyle>
+#include <QTranslator>
+#include "Application.h"
+#include "MainWindow.h"
+
+#ifdef Q_OS_WIN
+#include <stdio.h>
+#include <windows.h>
+#include <io.h>
+#include <fcntl.h>
+#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>.", "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>.", "n");
+        parser.addOption(optionSeed);
+        QCommandLineOption optionThreads("threads", "Use <n> 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 (file)
index 0000000..b74b6eb
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "MainWindow.h"
+
+#include <algorithm>
+#include <fstream>
+#include <QAction>
+#include <QApplication>
+#include <QDir>
+#include <QDesktopWidget>
+#include <QFileDialog>
+#include <QImageWriter>
+#include <QInputDialog>
+#include <QLabel>
+#include <QMenu>
+#include <QMenuBar>
+#include <QMessageBox>
+#include <QPlainTextEdit>
+#include <QPushButton>
+#include <QSettings>
+#include <QSplitter>
+#include <QStandardPaths>
+#include <QStatusBar>
+#include <QToolBar>
+#include <QToolButton>
+#include <QtConcurrentRun>
+#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<QToolBar*>()->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"),
+                       "<style type=\"text/css\">"
+                       ":link { text-decoration: none; }"
+                       "</style>"
+                       "<h2>" + tr("Pentobi") + "</h2>"
+                       "<p>" + tr("Version %1").arg(getVersion()) + "</p>"
+                       "<p>" +
+                       tr("Computer opponent for the board game Blokus.")
+                       + "<br>" +
+                       tr("&copy; 2011&ndash;%1 Markus Enzenberger").arg(2017) +
+                       + "<br>" +
+                       "<a href=\"http://pentobi.sourceforge.net\">http://pentobi.sourceforge.net</a>"
+                       "</p>");
+}
+
+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<ColorMove>&)),
+            SLOT(gotoPosition(Variant,const vector<ColorMove>&)));
+    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<bool> 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<Color::IntType>(
+                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<int>(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<QToolButton*>())
+    {
+        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<unsigned> 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 && &current != &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<QToolBar*>()->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<GenMoveResult> 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<const SgfNode*> 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<int>(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<ColorMove>& 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<QToolBar*>());
+    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<QToolBar*>()->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<QAction*>(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<Piece::IntType>(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<QAction*>(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<Piece::IntType>(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<float>(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<unsigned>(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?"),
+                 "<html>" +
+                 tr("In this game, you play %1 against Pentobi level&nbsp;%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<int>(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<int>(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<QAction*>(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<QToolBar*>()->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<QToolBar*>()->setToolButtonStyle(style);
+    if (m_helpWindow)
+        m_helpWindow->findChild<QToolBar*>()->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<QAction*>(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<bool>(out);
+}
+
+//-----------------------------------------------------------------------------
diff --git a/src/pentobi/MainWindow.h b/src/pentobi/MainWindow.h
new file mode 100644 (file)
index 0000000..fd9332c
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include <QElapsedTimer>
+#include <QFutureWatcher>
+#include <QMainWindow>
+#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<ColorMove>& 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<Player> 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<RatingHistory> m_history;
+
+    /** Local variable in findMove().
+        Reused for efficiency. */
+    unique_ptr<MoveMarker> m_marker;
+
+    GuiBoard* m_guiBoard;
+
+    QString m_helpDir;
+
+    ColorMap<bool> m_computerColors;
+
+    ColorMap<PieceSelector*> 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<GenMoveResult> m_genMoveWatcher;
+
+    QString m_file;
+
+    unique_ptr<MoveList> 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 (file)
index 0000000..ef4a51b
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "RatedGamesList.h"
+
+#include <QHeaderView>
+#include <QKeyEvent>
+#include <QStandardItemModel>
+#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<QKeyEvent*>(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<size_t>(numeric_limits<int>::max()))
+        nuRows = static_cast<int>(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 (file)
index 0000000..8426850
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include <QTableView>
+#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 (file)
index 0000000..40edf5b
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "RatingDialog.h"
+
+#include <QDialogButtonBox>
+#include <QFormLayout>
+#include <QFrame>
+#include <QLabel>
+#include <QMessageBox>
+#include <QPainter>
+#include <QPen>
+#include <QPushButton>
+#include <QSettings>
+#include <QVBoxLayout>
+#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<QAbstractButton*>(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("<b>--");
+        m_labelBestRating->setText("--");
+    }
+    else
+    {
+        m_labelRating->setText(QString("<b>%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 (file)
index 0000000..195409c
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include <QDialog>
+#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 (file)
index 0000000..c01d4f7
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "RatingGraph.h"
+
+#include <QApplication>
+#include <QDesktopWidget>
+#include <QPainter>
+#include <QPen>
+
+//-----------------------------------------------------------------------------
+
+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<float>(metrics.height()));
+        float bottomMargin = ceil(0.3f * static_cast<float>(metrics.height()));
+        float graphHeight =
+            static_cast<float>(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<int>(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 (file)
index 0000000..b737c9a
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include <QFrame>
+#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<float> m_values;
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_RATING_GRAPH_H
diff --git a/src/pentobi/RatingHistory.cpp b/src/pentobi/RatingHistory.cpp
new file mode 100644 (file)
index 0000000..5a27f62
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "RatingHistory.h"
+
+#include <fstream>
+#include <sstream>
+#include <QDir>
+#include <QFile>
+#include <QSettings>
+#include <QString>
+#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<Color::IntType>(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<Color::IntType>(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<double>(m_rating.get()));
+    settings.setValue("best_rating_" + variantStr,
+                      static_cast<double>(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<unsigned>(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 (file)
index 0000000..568fe5a
--- /dev/null
@@ -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 <vector>
+#include <QString>
+#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<GameInfo>& 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<GameInfo> m_games;
+};
+
+inline const vector<RatingHistory::GameInfo>& 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 (file)
index 0000000..b8e19f7
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "ShowMessage.h"
+
+#include <QMessageBox>
+#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"
+        " <a href=\"http://sf.net/p/pentobi/bugs\">bug tracker</a>.";
+    msgBox.setInformativeText("<html>" + 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 (file)
index 0000000..3cc3cdc
--- /dev/null
@@ -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 <QString>
+
+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 (file)
index 0000000..387b7a2
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "Util.h"
+
+#include <QCryptographicHash>
+#include <QDialog>
+#include <QDir>
+#include <QFileInfo>
+#include <QStandardPaths>
+#include <QString>
+#include <QUrl>
+#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 (file)
index 0000000..d5ba8cc
--- /dev/null
@@ -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 (file)
index 0000000..8731db4
Binary files /dev/null and b/src/pentobi/help/C/pentobi/analysis.jpg differ
diff --git a/src/pentobi/help/C/pentobi/become_stronger.html b/src/pentobi/help/C/pentobi/become_stronger.html
new file mode 100644 (file)
index 0000000..e1f203f
--- /dev/null
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="en">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="user_interface.html">Previous</a> | <a href=
+"window_menu.html">Next</a></p>
+<h2>Become a Stronger Player</h2>
+<p>Pentobi has functionality that can help you to become a stronger Blokus
+player.</p>
+<h3 id="analysis">Game Analysis</h3>
+<p>A game can be analyzed by selecting <i>Analyze Game</i> from the
+<i>Tools</i> 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.</p>
+<p align="center"><img src="analysis.jpg" alt="Game analysis window"></p>
+<div align="center" class="caption">Analysis of a game of variant Classic (2
+players).</div>
+<p>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.</p>
+<p>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 <i>Play Single Move</i> from the <i>Computer</i> menu.</p>
+<h3 id="rating">Determine Your Rating</h3>
+<p>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.</p>
+<p>A rated game is started with <i>Rated Game</i> from the <i>Game</i> 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.</p>
+<p>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.</p>
+<p>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.</p>
+<p>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.</p>
+<p align="center"><img src="rating.jpg" alt="Rating window"></p>
+<div align="center" class="caption">Window with rating graph.</div>
+<p>You can always see your current rating by selecting <i>Rating</i> from the
+<i>Tools</i> 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.</p>
+<p align="right"><a href="user_interface.html">Previous</a> | <a href=
+"window_menu.html">Next</a></p>
+</body>
+</html>
diff --git a/src/pentobi/help/C/pentobi/board_callisto.png b/src/pentobi/help/C/pentobi/board_callisto.png
new file mode 100644 (file)
index 0000000..f6e63bb
Binary files /dev/null and b/src/pentobi/help/C/pentobi/board_callisto.png differ
diff --git a/src/pentobi/help/C/pentobi/board_classic.png b/src/pentobi/help/C/pentobi/board_classic.png
new file mode 100644 (file)
index 0000000..cbbd188
Binary files /dev/null and b/src/pentobi/help/C/pentobi/board_classic.png differ
diff --git a/src/pentobi/help/C/pentobi/board_duo.png b/src/pentobi/help/C/pentobi/board_duo.png
new file mode 100644 (file)
index 0000000..f41d71b
Binary files /dev/null and b/src/pentobi/help/C/pentobi/board_duo.png differ
diff --git a/src/pentobi/help/C/pentobi/board_nexos.png b/src/pentobi/help/C/pentobi/board_nexos.png
new file mode 100644 (file)
index 0000000..65c55d0
Binary files /dev/null and b/src/pentobi/help/C/pentobi/board_nexos.png differ
diff --git a/src/pentobi/help/C/pentobi/board_trigon.jpg b/src/pentobi/help/C/pentobi/board_trigon.jpg
new file mode 100644 (file)
index 0000000..e75d89c
Binary files /dev/null and b/src/pentobi/help/C/pentobi/board_trigon.jpg differ
diff --git a/src/pentobi/help/C/pentobi/callisto_rules.html b/src/pentobi/help/C/pentobi/callisto_rules.html
new file mode 100644 (file)
index 0000000..ef5a189
--- /dev/null
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="en">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="nexos_rules.html">Previous</a> | <a href=
+"user_interface.html">Next</a></p>
+<h2>Callisto Rules</h2>
+<p>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.</p>
+<p align="center"><img src="pieces_callisto.png" alt=
+"Pieces for game variant Callisto"></p>
+<div align="center" class="caption">The 21 pieces.</div>
+<p>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.</p>
+<p align="center"><img src="board_callisto.png" alt=
+"Board for game variant Callisto"></p>
+<div align="center" class="caption">The board with the center having a darker
+color.</div>
+<p>All larger pieces may be placed anwhere on the board but must touch an
+existing piece of the same color edge-to-edge.</p>
+<p align="center"><img src="position_callisto.png" alt=
+"Example position for game variant Callisto"></p>
+<div align="center" class="caption">An example position after a few
+moves.</div>
+<p>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.</p>
+<h3>Rules for two or three players</h3>
+<p>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.</p>
+<p align="right"><a href="nexos_rules.html">Previous</a> | <a href=
+"user_interface.html">Next</a></p>
+</body>
+</html>
diff --git a/src/pentobi/help/C/pentobi/classic_rules.html b/src/pentobi/help/C/pentobi/classic_rules.html
new file mode 100644 (file)
index 0000000..1b6f1c4
--- /dev/null
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="en">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="index.html">Previous</a> | <a href=
+"duo_rules.html">Next</a></p>
+<h2>Classic Rules</h2>
+<p>There are four players, Blue, Yellow, Red and Green, and a board consisting
+of 20×20 squares.</p>
+<p>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.)</p>
+<p align="center"><img src="pieces.png" alt=
+"Pieces for game variant Classic"></p>
+<div align="center" class="caption">The 21 pieces.</div>
+<p>The players alternate in placing one of their pieces on the board. Blue
+starts, followed by Yellow, then Red, then Green.</p>
+<p>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.</p>
+<p align="center"><img src="board_classic.png" alt=
+"Board for game variant Classic"></p>
+<div align="center" class="caption">The 20×20 board with the starting<br>
+squares marked with colored dots.</div>
+<p>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.</p>
+<p align="center"><img src="position_classic.png" alt=
+"Example position for game variant Classic"></p>
+<div align="center" class="caption">An example position after a few
+moves.</div>
+<p>When the player of a color cannot place any more pieces, the player passes
+and the next color continues.</p>
+<p>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.</p>
+<h3>Rules for Two Players</h3>
+<p>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.</p>
+<h3>Rules for Three Players</h3>
+<p>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.</p>
+<h3>Colorless starting points</h3>
+<p>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.</p>
+<p align="right"><a href="index.html">Previous</a> | <a href=
+"duo_rules.html">Next</a></p>
+</body>
+</html>
diff --git a/src/pentobi/help/C/pentobi/duo_rules.html b/src/pentobi/help/C/pentobi/duo_rules.html
new file mode 100644 (file)
index 0000000..53dba8d
--- /dev/null
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="en">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="classic_rules.html">Previous</a> | <a href=
+"trigon_rules.html">Next</a></p>
+<h2>Duo Rules</h2>
+<p>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.</p>
+<p align="center"><img src="board_duo.png" alt=
+"Board for game variant Duo"></p>
+<div align="center" class="caption">The 14×14 board used in game variant Duo
+with<br>
+the starting squares marked with colored dots.</div>
+<p align="center"><img src="position_duo.png" alt=
+"Example position for game variant Duo"></p>
+<div align="center" class="caption">An example position in game variant
+Duo.</div>
+<p align="right"><a href="classic_rules.html">Previous</a> | <a href=
+"trigon_rules.html">Next</a></p>
+</body>
+</html>
diff --git a/src/pentobi/help/C/pentobi/index.html b/src/pentobi/help/C/pentobi/index.html
new file mode 100644 (file)
index 0000000..8e5329d
--- /dev/null
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="en">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="classic_rules.html">Next</a></p>
+<h1>Pentobi</h1>
+<p>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.</p>
+<p><a href="classic_rules.html">Classic Rules</a><br>
+<a href="duo_rules.html">Duo Rules</a><br>
+<a href="trigon_rules.html">Trigon Rules</a><br>
+<a href="junior_rules.html">Junior Rules</a><br>
+<a href="nexos_rules.html">Nexos Rules</a><br>
+<a href="callisto_rules.html">Callisto Rules</a><br>
+<a href="user_interface.html">How to Use Pentobi</a><br>
+<a href="become_stronger.html">Become a Stronger Player</a><br>
+<a href="window_menu.html">The Window Menu</a><br>
+<a href="shortcuts.html">Keyboard Shortcuts</a><br>
+<a href="system.html">System Requirements</a><br>
+<a href="license.html">License</a></p>
+</body>
+</html>
diff --git a/src/pentobi/help/C/pentobi/junior_rules.html b/src/pentobi/help/C/pentobi/junior_rules.html
new file mode 100644 (file)
index 0000000..ce2ff38
--- /dev/null
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="en">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="trigon_rules.html">Previous</a> | <a href=
+"nexos_rules.html">Next</a></p>
+<h2>Junior Rules</h2>
+<p>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.</p>
+<p align="center"><img src="pieces_junior.png" alt=
+"Pieces for game variant Junior"></p>
+<div align="center" class="caption">The 24 pieces used in Junior.</div>
+<p>Bonus points are not used in Junior.</p>
+<p align="right"><a href="trigon_rules.html">Previous</a> | <a href=
+"nexos_rules.html">Next</a></p>
+</body>
+</html>
diff --git a/src/pentobi/help/C/pentobi/license.html b/src/pentobi/help/C/pentobi/license.html
new file mode 100644 (file)
index 0000000..6e97dcf
--- /dev/null
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="en">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="system.html">Previous</a></p>
+<h2>License</h2>
+<p>Copyright © 2011–2017 Markus Enzenberger</p>
+<p>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.</p>
+<p>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.</p>
+<h3>Trademark Disclaimer</h3>
+<p>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.</p>
+<p align="right"><a href="system.html">Previous</a></p>
+</body>
+</html>
diff --git a/src/pentobi/help/C/pentobi/nexos_rules.html b/src/pentobi/help/C/pentobi/nexos_rules.html
new file mode 100644 (file)
index 0000000..5e51e81
--- /dev/null
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="en">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="junior_rules.html">Previous</a> | <a href=
+"callisto_rules.html">Next</a></p>
+<h2>Nexos Rules</h2>
+<p>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.</p>
+<p align="center"><img src="pieces_nexos.png" alt="Pieces for Nexos"></p>
+<div align="center" class="caption">The 24 pieces.</div>
+<p>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.</p>
+<p align="center"><img src="board_nexos.png" alt="Board for Nexos"></p>
+<div align="center" class="caption">The board for Nexos with segments touching
+the<br>
+starting intersections marked with colored dots.</div>
+<p>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.</p>
+<p align="center"><img src="position_nexos.png" alt=
+"Example position for Nexos"></p>
+<div align="center" class="caption">An example position after a few
+moves.</div>
+<p>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.</p>
+<h3>Rules for Two Players</h3>
+<p>Like Blokus, Nexos can be played with two players by having one player play
+Blue and Red and the other player Yellow and Green.</p>
+<p align="right"><a href="junior_rules.html">Previous</a> | <a href=
+"callisto_rules.html">Next</a></p>
+</body>
+</html>
diff --git a/src/pentobi/help/C/pentobi/pieces.png b/src/pentobi/help/C/pentobi/pieces.png
new file mode 100644 (file)
index 0000000..4af2864
Binary files /dev/null and b/src/pentobi/help/C/pentobi/pieces.png differ
diff --git a/src/pentobi/help/C/pentobi/pieces_callisto.png b/src/pentobi/help/C/pentobi/pieces_callisto.png
new file mode 100644 (file)
index 0000000..976efdc
Binary files /dev/null and b/src/pentobi/help/C/pentobi/pieces_callisto.png differ
diff --git a/src/pentobi/help/C/pentobi/pieces_junior.png b/src/pentobi/help/C/pentobi/pieces_junior.png
new file mode 100644 (file)
index 0000000..6940d2f
Binary files /dev/null and b/src/pentobi/help/C/pentobi/pieces_junior.png differ
diff --git a/src/pentobi/help/C/pentobi/pieces_nexos.png b/src/pentobi/help/C/pentobi/pieces_nexos.png
new file mode 100644 (file)
index 0000000..9595666
Binary files /dev/null and b/src/pentobi/help/C/pentobi/pieces_nexos.png differ
diff --git a/src/pentobi/help/C/pentobi/pieces_trigon.jpg b/src/pentobi/help/C/pentobi/pieces_trigon.jpg
new file mode 100644 (file)
index 0000000..bc9457b
Binary files /dev/null and b/src/pentobi/help/C/pentobi/pieces_trigon.jpg differ
diff --git a/src/pentobi/help/C/pentobi/position_callisto.png b/src/pentobi/help/C/pentobi/position_callisto.png
new file mode 100644 (file)
index 0000000..1d4d1bd
Binary files /dev/null and b/src/pentobi/help/C/pentobi/position_callisto.png differ
diff --git a/src/pentobi/help/C/pentobi/position_classic.png b/src/pentobi/help/C/pentobi/position_classic.png
new file mode 100644 (file)
index 0000000..7b73e4b
Binary files /dev/null and b/src/pentobi/help/C/pentobi/position_classic.png differ
diff --git a/src/pentobi/help/C/pentobi/position_duo.png b/src/pentobi/help/C/pentobi/position_duo.png
new file mode 100644 (file)
index 0000000..bc6c8e2
Binary files /dev/null and b/src/pentobi/help/C/pentobi/position_duo.png differ
diff --git a/src/pentobi/help/C/pentobi/position_nexos.png b/src/pentobi/help/C/pentobi/position_nexos.png
new file mode 100644 (file)
index 0000000..1d66da4
Binary files /dev/null and b/src/pentobi/help/C/pentobi/position_nexos.png differ
diff --git a/src/pentobi/help/C/pentobi/position_trigon.jpg b/src/pentobi/help/C/pentobi/position_trigon.jpg
new file mode 100644 (file)
index 0000000..6f6aedc
Binary files /dev/null and b/src/pentobi/help/C/pentobi/position_trigon.jpg differ
diff --git a/src/pentobi/help/C/pentobi/rating.jpg b/src/pentobi/help/C/pentobi/rating.jpg
new file mode 100644 (file)
index 0000000..e92aa99
Binary files /dev/null and b/src/pentobi/help/C/pentobi/rating.jpg differ
diff --git a/src/pentobi/help/C/pentobi/shortcuts.html b/src/pentobi/help/C/pentobi/shortcuts.html
new file mode 100644 (file)
index 0000000..ff2e75d
--- /dev/null
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="en">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="window_menu.html">Previous</a> | <a href=
+"system.html">Next</a></p>
+<h2>Keyboard Shortcuts</h2>
+<p>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.</p>
+<dl>
+<dt>Plus</dt>
+<dd>
+<p>Select next piece</p>
+</dd>
+<dt>Minus</dt>
+<dd>
+<p>Select previous piece</p>
+</dd>
+<dt>0</dt>
+<dd>
+<p>Clear selected piece</p>
+</dd>
+<dt>Space</dt>
+<dd>
+<p>Next orientation of the selected piece</p>
+</dd>
+<dt>Shift+Space</dt>
+<dd>
+<p>Previous orientation of the selected piece</p>
+</dd>
+<dt>Left, Right, Up, Down</dt>
+<dd>
+<p>Move the selected piece.</p>
+</dd>
+<dt>Enter</dt>
+<dd>
+<p>Play the selected piece.</p>
+</dd>
+<dt>1, 2, A, C, E, F, G, H, I, J, L, N, O, P, S, T, U, V, W, X, Y, Z</dt>
+<dd>
+<p>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").</p>
+</dd>
+</dl>
+<p align="right"><a href="window_menu.html">Previous</a> | <a href=
+"system.html">Next</a></p>
+</body>
+</html>
diff --git a/src/pentobi/help/C/pentobi/stylesheet.css b/src/pentobi/help/C/pentobi/stylesheet.css
new file mode 100644 (file)
index 0000000..62522b0
--- /dev/null
@@ -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 (file)
index 0000000..c01c0e4
--- /dev/null
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="en">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="shortcuts.html">Previous</a> | <a href=
+"license.html">Next</a></p>
+<h2>System Requirements</h2>
+<p>Minimum: 1&nbsp;GB RAM, 1&nbsp;GHz CPU<br>
+Recommended for playing level 9: 4&nbsp;GB RAM, 2&nbsp;GHz dual-core or faster
+CPU</p>
+<p>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).</p>
+<p align="right"><a href="shortcuts.html">Previous</a> | <a href=
+"license.html">Next</a></p>
+</body>
+</html>
diff --git a/src/pentobi/help/C/pentobi/trigon_rules.html b/src/pentobi/help/C/pentobi/trigon_rules.html
new file mode 100644 (file)
index 0000000..6668322
--- /dev/null
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="en">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="duo_rules.html">Previous</a> | <a href=
+"junior_rules.html">Next</a></p>
+<h2>Trigon Rules</h2>
+<p>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.)</p>
+<p align="center"><img src="pieces_trigon.jpg" alt=
+"Pieces for game variant Trigon"></p>
+<div align="center" class="caption">The 22 Trigon pieces.</div>
+<p>The board also consists of triangles and is shaped like a hexagon with an
+edge size of nine triangles.</p>
+<p align="center"><img src="board_trigon.jpg" alt=
+"Board for game variant Trigon"></p>
+<div align="center" class="caption">The board with the starting<br>
+fields marked with gray dots.</div>
+<p>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.</p>
+<p align="center"><img src="position_trigon.jpg" alt=
+"Example position for game variant Trigon"></p>
+<div align="center" class="caption">An example position after a few
+moves.</div>
+<h3>Rules for Two Players</h3>
+<p>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.</p>
+<h3>Rules for Three Players</h3>
+<p>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.</p>
+<p align="right"><a href="duo_rules.html">Previous</a> | <a href=
+"junior_rules.html">Next</a></p>
+</body>
+</html>
diff --git a/src/pentobi/help/C/pentobi/user_interface.html b/src/pentobi/help/C/pentobi/user_interface.html
new file mode 100644 (file)
index 0000000..1e0928d
--- /dev/null
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="en">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="callisto_rules.html">Previous</a> | <a href=
+"become_stronger.html">Next</a></p>
+<h2>How to Use Pentobi</h2>
+<h3>Board</h3>
+<p>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).</p>
+<p>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.</p>
+<h3>Pieces and Score</h3>
+<p>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.</p>
+<p>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
+<a href="shortcuts.html">shortcut keys</a>.</p>
+<p>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.</p>
+<h3>Playing Against the Computer</h3>
+<p>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.</p>
+<p>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
+<i>Computer Colors</i> from the <i>Computer</i> menu or toolbar and select the
+colors the computer should play.</p>
+<p>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 <i>Computer Colors</i> dialog only once and it will stay that
+way. After loading a saved game, the computer also plays no color by
+default.</p>
+<p>Selecting <i>Play</i> from the <i>Computer</i> 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.</p>
+<h3>Move Variations and the Game Tree</h3>
+<p>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 <i>Go</i> menu or in the toolbar.</p>
+<p>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 <i>Forward</i> in the <i>Go</i> 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 <i>Make Main Variation</i> from the
+<i>Edit</i> menu.</p>
+<p align="right"><a href="callisto_rules.html">Previous</a> | <a href=
+"become_stronger.html">Next</a></p>
+</body>
+</html>
diff --git a/src/pentobi/help/C/pentobi/window_menu.html b/src/pentobi/help/C/pentobi/window_menu.html
new file mode 100644 (file)
index 0000000..51c56c4
--- /dev/null
@@ -0,0 +1,201 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="en">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="become_stronger.html">Previous</a> | <a href=
+"shortcuts.html">Next</a></p>
+<h2>The Window Menu</h2>
+<h3>Game</h3>
+<dl>
+<dt>New</dt>
+<dd>Start a new game.</dd>
+<dt>Rated Game</dt>
+<dd>Start a new <a href="become_stronger.html#rating">rated game</a> against
+the computer.</dd>
+<dt>Game Variant</dt>
+<dd>Select a game variant and start a new game of this game variant.</dd>
+<dt>Game Info</dt>
+<dd>Display or edit additional information about the game like the name of the
+players or the date when the game was played.</dd>
+<dt>Undo Move</dt>
+<dd>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 <i>Edit/Truncate</i> to remove inner nodes of the
+game tree).</dd>
+<dt>Find Move</dt>
+<dd>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.</dd>
+<dt>Open</dt>
+<dd>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.</dd>
+<dt>Open Recent</dt>
+<dd>Load a recently used game.</dd>
+<dt>Save</dt>
+<dd>Save the current game.</dd>
+<dt>Save As</dt>
+<dd>Save the current game under a new file name.</dd>
+<dt>Export/Image</dt>
+<dd>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).</dd>
+<dt>Export/ASCII Art</dt>
+<dd>Save the current position as a text diagram. The text diagram should be
+viewed using a monospace font.</dd>
+<dt>Quit</dt>
+<dd>Quit Pentobi.</dd>
+</dl>
+<h3>Go</h3>
+<dl>
+<dt>Beginning</dt>
+<dd>Go to the beginning of the game.</dd>
+<dt>Backward</dt>
+<dd>Go one move backward in the current variation. The corresponding button in
+the toolbar supports autorepeat if pressed and held.</dd>
+<dt>Forward</dt>
+<dd>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.</dd>
+<dt>End</dt>
+<dd>Go to the end of the current variation. Like <i>Forward</i>, this also uses
+the first variation in positions with several follow-up variations.</dd>
+<dt>Next Variation</dt>
+<dd>Go to the next variation to the last move played (i.e. the next sibling
+node of the current node in the game tree).</dd>
+<dt>Previous Variation</dt>
+<dd>Go to the previous variation to the last move played (i.e. the previous
+sibling node of the current node in the game tree).</dd>
+<dt>Go to Move</dt>
+<dd>Go to the move with a given move number in the current variation.</dd>
+<dt>Back to Main Variation</dt>
+<dd>Go back to the last position in the current variation that belonged to the
+main variation.</dd>
+<dt>Beginning of Branch</dt>
+<dd>Go back to the last position in the current variation that had an
+alternative move.</dd>
+<dt>Find Next Comment</dt>
+<dd>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.</dd>
+</dl>
+<h3>Edit</h3>
+<dl>
+<dt>Move Annotation</dt>
+<dd>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 <i>Move Marking</i>, on the board.</dd>
+<dt>Make Main Variation</dt>
+<dd>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.</dd>
+<dt>Move Variation Up</dt>
+<dd>Changes the order of variations such that the current position will appear
+earlier when iterating over the variations with <i>Next/Previous
+Variation</i>.</dd>
+<dt>Move Variation Down</dt>
+<dd>Changes the order of variations such that the current position will appear
+later when iterating over the variations with <i>Next/Previous
+Variation</i>.</dd>
+<dt>Delete All Variations</dt>
+<dd>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 <i>Back
+to Main Variation</i>.</dd>
+<dt>Truncate</dt>
+<dd>Remove the node with the current position, including any subtree, from the
+game tree.</dd>
+<dt>Truncate Children</dt>
+<dd>Remove all child nodes of the node with the current position from the game
+tree.</dd>
+<dt>Keep Only Position</dt>
+<dd>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.</dd>
+<dt>Keep Only Subtree</dt>
+<dd>Like <i>Keep Only Position</i> but does not delete the moves after the
+current position.</dd>
+<dt>Setup Mode</dt>
+<dd>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 <i>Next Color</i> 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.</dd>
+<dt>Next Color</dt>
+<dd>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.</dd>
+</dl>
+<h3>View</h3>
+<dl>
+<dt>Toolbar</dt>
+<dd>Show or hide the toolbar.</dd>
+<dt>Toolbar Text</dt>
+<dd>Configure the appearance of the toolbar.</dd>
+<dt>Comment</dt>
+<dd>Show or hide a text field to display or edit comments on the current
+position.</dd>
+<dt>Move Marking</dt>
+<dd>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.</dd>
+<dt>Coordinates</dt>
+<dd>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.</dd>
+<dt>Show Variations</dt>
+<dd>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.</dd>
+<dt>Fullscreen</dt>
+<dd>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.</dd>
+</dl>
+<h3>Computer</h3>
+<dl>
+<dt>Computer Colors</dt>
+<dd>Select which colors are played by the computer.</dd>
+<dt>Play</dt>
+<dd>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.</dd>
+<dt>Play Single Move</dt>
+<dd>Make the computer play a single move for the current color without changing
+the colors played by the computer.</dd>
+<dt>Stop</dt>
+<dd>Abort the current move generation. You can make the computer continue to
+play by selecting <i>Play</i>.</dd>
+<dt>Level</dt>
+<dd>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.</dd>
+</dl>
+<h3>Tools</h3>
+<dl>
+<dt>Rating</dt>
+<dd>Show a dialog window with the <a href=
+"become_stronger.html#rating">rating</a> of the user in the current game
+variant.</dd>
+<dt>Analyze Game</dt>
+<dd>Perform a <a href="become_stronger.html#analysis">game analysis</a>.</dd>
+</dl>
+<h3>Help</h3>
+<dl>
+<dt>Pentobi Help</dt>
+<dd>Show a window to browse the Pentobi user manual.</dd>
+<dt>About Pentobi</dt>
+<dd>Show an info dialog with information about this version of Pentobi.</dd>
+</dl>
+<p align="right"><a href="become_stronger.html">Previous</a> | <a href=
+"shortcuts.html">Next</a></p>
+</body>
+</html>
diff --git a/src/pentobi/help/de/pentobi/become_stronger.html b/src/pentobi/help/de/pentobi/become_stronger.html
new file mode 100644 (file)
index 0000000..6f50443
--- /dev/null
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="de">
+<head>
+<title>Pentobi-Hilfe</title>
+<link rel="stylesheet" type="text/css" href="../../C/pentobi/stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="user_interface.html">Zurück</a> | <a href=
+"window_menu.html">Weiter</a></p>
+<h2>Ein stärkerer Spieler werden</h2>
+<p>Pentobi besitzt Funktionen, die Ihnen helfen können, ein stärkerer
+Blokus-Spieler zu werden.</p>
+<h3 id="analysis">Spielanalyse</h3>
+<p>Sie können ein Spiel analysieren, indem Sie <i>Spiel analysieren</i> aus dem
+<i>Extras</i>-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.</p>
+<p align="center"><img src="../../C/pentobi/analysis.jpg" alt=
+"Spielanalyse-Fenster"></p>
+<div align="center" class="caption">Analyse eines Spiels der Spielvariante
+Klassisch (2 Spieler).</div>
+<p>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.</p>
+<p>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
+<i>Einzelnen Zug spielen</i> aus dem <i>Computer</i>-Menü auswählen.</p>
+<h3 id="rating">Ihre Wertung ermitteln</h3>
+<p>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.</p>
+<p>Ein gewertetes Spiel wird mit <i>Gewertetes Spiel</i> aus dem
+<i>Spiel</i>-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.</p>
+<p>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.</p>
+<p>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.</p>
+<p>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.</p>
+<p align="center"><img src="../../C/pentobi/rating.jpg" alt=
+"Wertungsfenster"></p>
+<div align="center" class="caption">Fenster mit Wertungsgraph.</div>
+<p>Sie können Ihre aktuelle Wertung jederzeit mit <i>Wertung</i> aus dem
+<i>Extras</i>-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.</p>
+<p align="right"><a href="user_interface.html">Zurück</a> | <a href=
+"window_menu.html">Weiter</a></p>
+</body>
+</html>
diff --git a/src/pentobi/help/de/pentobi/callisto_rules.html b/src/pentobi/help/de/pentobi/callisto_rules.html
new file mode 100644 (file)
index 0000000..8c53e36
--- /dev/null
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="de">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="../../C/pentobi/stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="nexos_rules.html">Zurück</a> | <a href=
+"user_interface.html">Weiter</a></p>
+<h2>Callisto-Regeln</h2>
+<p>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.</p>
+<p align="center"><img src="../../C/pentobi/pieces_callisto.png" alt=
+"Spielsteine für Spielvariante Callisto"></p>
+<div align="center" class="caption">Die 21 Spielsteine.</div>
+<p>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.</p>
+<p align="center"><img src="../../C/pentobi/board_callisto.png" alt=
+"Spielbrett für Spielvariante Callisto"></p>
+<div align="center" class="caption">Das Brett mit einer dunkleren Farbe im
+Zentrum.</div>
+<p>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.</p>
+<p align="center"><img src="../../C/pentobi/position_callisto.png" alt=
+"Beispielstellung für Spielvariante Callisto"></p>
+<div align="center" class="caption">Eine Beispielstellung nach ein paar
+Zügen.</div>
+<p>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.</p>
+<h3>Regeln für zwei oder drei Spieler</h3>
+<p>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.</p>
+<p align="right"><a href="nexos_rules.html">Zurück</a> | <a href=
+"user_interface.html">Weiter</a></p>
+</body>
+</html>
diff --git a/src/pentobi/help/de/pentobi/classic_rules.html b/src/pentobi/help/de/pentobi/classic_rules.html
new file mode 100644 (file)
index 0000000..fefcf6d
--- /dev/null
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="de">
+<head>
+<title>Pentobi-Hilfe</title>
+<link rel="stylesheet" type="text/css" href="../../C/pentobi/stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="index.html">Zurück</a> | <a href=
+"duo_rules.html">Weiter</a></p>
+<h2>Klassische Regeln</h2>
+<p>Es gibt vier Spieler, Blau, Gelb, Rot und Grün, und ein Brett, das aus 20×20
+Quadraten besteht.</p>
+<p>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).</p>
+<p align="center"><img src="../../C/pentobi/pieces.png" alt=
+"Spielsteine für Spielvariante Klassisch"></p>
+<div align="center" class="caption">Die 21 Spielsteine.</div>
+<p>Die Spieler setzen abwechselnd einen ihrer Spielsteine aufs Brett. Blau
+fängt an, gefolgt von Gelb, dann Rot, dann Grün.</p>
+<p>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.</p>
+<p align="center"><img src="../../C/pentobi/board_classic.png" alt=
+"Spielbrett für Spielvariante Klassisch"></p>
+<div align="center" class="caption">Das 20×20-Brett mit den Startfeldern<br>
+durch farbige Punkte markiert.</div>
+<p>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.</p>
+<p align="center"><img src="../../C/pentobi/position_classic.png" alt=
+"Beispielstellung für Spielvariante Klassisch"></p>
+<div align="center" class="caption">Eine Beispielstellung nach ein paar
+Zügen.</div>
+<p>Wenn der Spieler einer Farbe keine Spielsteine mehr setzen kann, muss der
+Spieler aussetzen und die nächste Farbe ist am Zug.</p>
+<p>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.</p>
+<h3>Regeln für zwei Spieler</h3>
+<p>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.</p>
+<h3>Regeln für drei Spieler</h3>
+<p>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.</p>
+<h3>Farblose Startfelder</h3>
+<p>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.</p>
+<p align="right"><a href="index.html">Zurück</a> | <a href=
+"duo_rules.html">Weiter</a></p>
+</body>
+</html>
diff --git a/src/pentobi/help/de/pentobi/duo_rules.html b/src/pentobi/help/de/pentobi/duo_rules.html
new file mode 100644 (file)
index 0000000..bdc1318
--- /dev/null
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="de">
+<head>
+<title>Pentobi-Hilfe</title>
+<link rel="stylesheet" type="text/css" href="../../C/pentobi/stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="classic_rules.html">Zurück</a> | <a href=
+"trigon_rules.html">Weiter</a></p>
+<h2>Duo-Regeln</h2>
+<p>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.</p>
+<p align="center"><img src="../../C/pentobi/board_duo.png" alt=
+"Spielbrett für Spielvariante Duo"></p>
+<div align="center" class="caption">Das 14×14-Brett, das in der Spielvariante
+Duo benutzt<br>
+wird, mit den Startfeldern durch farbige Punkte markiert.</div>
+<p align="center"><img src="../../C/pentobi/position_duo.png" alt=
+"Beispielstellung für Spielvariante Duo"></p>
+<div align="center" class="caption">Eine Beispielstellung in der Spielvariante
+Duo.</div>
+<p align="right"><a href="classic_rules.html">Zurück</a> | <a href=
+"trigon_rules.html">Weiter</a></p>
+</body>
+</html>
diff --git a/src/pentobi/help/de/pentobi/index.html b/src/pentobi/help/de/pentobi/index.html
new file mode 100644 (file)
index 0000000..6d22c49
--- /dev/null
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="de">
+<head>
+<title>Pentobi-Hilfe</title>
+<link rel="stylesheet" type="text/css" href="../../C/pentobi/stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="classic_rules.html">Weiter</a></p>
+<h1>Pentobi</h1>
+<p>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.</p>
+<p><a href="classic_rules.html">Klassische Regeln</a><br>
+<a href="duo_rules.html">Duo-Regeln</a><br>
+<a href="trigon_rules.html">Trigon-Regeln</a><br>
+<a href="junior_rules.html">Junior-Regeln</a><br>
+<a href="nexos_rules.html">Nexos-Regeln</a><br>
+<a href="callisto_rules.html">Callisto-Regeln</a><br>
+<a href="user_interface.html">Wie Sie Pentobi benutzen</a><br>
+<a href="become_stronger.html">Ein stärkerer Spieler werden</a><br>
+<a href="window_menu.html">Das Fenstermenü</a><br>
+<a href="shortcuts.html">Tastenkürzel</a><br>
+<a href="system.html">Systemvoraussetzungen</a><br>
+<a href="license.html">Lizenz</a></p>
+</body>
+</html>
diff --git a/src/pentobi/help/de/pentobi/junior_rules.html b/src/pentobi/help/de/pentobi/junior_rules.html
new file mode 100644 (file)
index 0000000..15f9ba9
--- /dev/null
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="de">
+<head>
+<title>Pentobi-Hilfe</title>
+<link rel="stylesheet" type="text/css" href="../../C/pentobi/stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="trigon_rules.html">Zurück</a> | <a href=
+"nexos_rules.html">Weiter</a></p>
+<h2>Junior-Regeln</h2>
+<p>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.</p>
+<p align="center"><img src="../../C/pentobi/pieces_junior.png" alt=
+"Spielsteine für Spielvariante Junior"></p>
+<div align="center" class="caption">Die 24 Spielsteine, die in Junior benutzt
+werden.</div>
+<p>Bonuspunkte werden in Junior nicht benutzt.</p>
+<p align="right"><a href="trigon_rules.html">Zurück</a> | <a href=
+"nexos_rules.html">Weiter</a></p>
+</body>
+</html>
diff --git a/src/pentobi/help/de/pentobi/license.html b/src/pentobi/help/de/pentobi/license.html
new file mode 100644 (file)
index 0000000..e4e2b74
--- /dev/null
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="de">
+<head>
+<title>Pentobi-Hilfe</title>
+<link rel="stylesheet" type="text/css" href="../../C/pentobi/stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="system.html">Zurück</a></p>
+<h2>Lizenz</h2>
+<p>Copyright © 2011–2017 Markus Enzenberger</p>
+<p>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.</p>
+<p>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.</p>
+<h3>Hinweis zu Markennamen</h3>
+<p>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.</p>
+<p align="right"><a href="system.html">Zurück</a></p>
+</body>
+</html>
diff --git a/src/pentobi/help/de/pentobi/nexos_rules.html b/src/pentobi/help/de/pentobi/nexos_rules.html
new file mode 100644 (file)
index 0000000..84d0e1d
--- /dev/null
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="de">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="../../C/pentobi/stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="junior_rules.html">Zurück</a> | <a href=
+"callisto_rules.html">Weiter</a></p>
+<h2>Nexos-Regeln</h2>
+<p>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.</p>
+<p align="center"><img src="../../C/pentobi/pieces_nexos.png" alt=
+"Spielsteine für Nexos"></p>
+<div align="center" class="caption">Die 24 Spielsteine.</div>
+<p>Jede Farbe hat einen Startkreuzungspunkt auf der Kreuzung der dritten Linien
+nahe einer Ecke. Der erste Spielstein muss den Startkreuzungspunkt
+berühren.</p>
+<p align="center"><img src="../../C/pentobi/board_nexos.png" alt=
+"Spielbrett für Nexos"></p>
+<div align="center" class="caption">Das Brett für Nexos mit den die
+Startkreuzungspunkte<br>
+berührenden Segmenten durch farbige Punkte markiert.</div>
+<p>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.</p>
+<p align="center"><img src="../../C/pentobi/position_nexos.png" alt=
+"Beispielstellung für Nexos"></p>
+<div align="center" class="caption">Eine Beispielstellung nach ein paar
+Zügen.</div>
+<p>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.</p>
+<h3>Regeln für zwei Spieler</h3>
+<p>Wie Blokus kann Nexos von zwei Spielern gespielt werden, indem ein Spieler
+Rot und Blau, und der andere Spieler Gelb und Grün spielt.</p>
+<p align="right"><a href="junior_rules.html">Zurück</a> | <a href=
+"callisto_rules.html">Weiter</a></p>
+</body>
+</html>
diff --git a/src/pentobi/help/de/pentobi/shortcuts.html b/src/pentobi/help/de/pentobi/shortcuts.html
new file mode 100644 (file)
index 0000000..3805109
--- /dev/null
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="de">
+<head>
+<title>Pentobi-Hilfe</title>
+<link rel="stylesheet" type="text/css" href="../../C/pentobi/stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="window_menu.html">Zurück</a> | <a href=
+"system.html">Weiter</a></p>
+<h2>Tastenkürzel</h2>
+<p>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.</p>
+<dl>
+<dt>Plus</dt>
+<dd>
+<p>Nächsten Spielstein auswählen</p>
+</dd>
+<dt>Minus</dt>
+<dd>
+<p>Vorherigen Spielstein auswählen</p>
+</dd>
+<dt>0</dt>
+<dd>
+<p>Spielsteinauswahl löschen</p>
+</dd>
+<dt>Leertaste</dt>
+<dd>
+<p>Nächste Ausrichtung des ausgewählten Spielsteins</p>
+</dd>
+<dt>Umschalt+Leertaste</dt>
+<dd>
+<p>Vorherige Ausrichtung des ausgewählten Spielsteins</p>
+</dd>
+<dt>Links, Rechts, Oben, Unten</dt>
+<dd>
+<p>Bewegen des ausgewählten Spielsteins.</p>
+</dd>
+<dt>Enter</dt>
+<dd>
+<p>Spielen des ausgewählten Spielsteins.</p>
+</dd>
+<dt>1, 2, A, C, E, F, G, H, I, J, L, N, O, P, S, T, U, V, W, X, Y, Z</dt>
+<dd>
+<p>Einen Spielstein entsprechend den üblicherweise benutzten Spielsteinnamen
+auswählen. Wenn es mehrere Spielsteine mit dem Buchstaben gibt (z.&nbsp;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“).</p>
+</dd>
+</dl>
+<p align="right"><a href="window_menu.html">Zurück</a> | <a href=
+"system.html">Weiter</a></p>
+</body>
+</html>
diff --git a/src/pentobi/help/de/pentobi/system.html b/src/pentobi/help/de/pentobi/system.html
new file mode 100644 (file)
index 0000000..26ed70c
--- /dev/null
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="de">
+<head>
+<title>Pentobi-Hilfe</title>
+<link rel="stylesheet" type="text/css" href="../../C/pentobi/stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="shortcuts.html">Zurück</a> | <a href=
+"license.html">Weiter</a></p>
+<h2>Systemvoraussetzungen</h2>
+<p>Minimum: 1&nbsp;GB RAM, 1&nbsp;GHz CPU<br>
+Empfohlen für Spielstufe 9: 4&nbsp;GB RAM, 2&nbsp;GHz Dual-Core- oder schnellere
+CPU</p>
+<p>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).</p>
+<p align="right"><a href="shortcuts.html">Zurück</a> | <a href=
+"license.html">Weiter</a></p>
+</body>
+</html>
diff --git a/src/pentobi/help/de/pentobi/trigon_rules.html b/src/pentobi/help/de/pentobi/trigon_rules.html
new file mode 100644 (file)
index 0000000..d4b99cf
--- /dev/null
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="de">
+<head>
+<title>Pentobi-Hilfe</title>
+<link rel="stylesheet" type="text/css" href="../../C/pentobi/stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="duo_rules.html">Zurück</a> | <a href=
+"junior_rules.html">Weiter</a></p>
+<h2>Trigon-Regeln</h2>
+<p>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).</p>
+<p align="center"><img src="../../C/pentobi/pieces_trigon.jpg" alt=
+"Spielsteine für Spielvariante Trigon"></p>
+<div align="center" class="caption">Die 22 Trigon-Spielsteine.</div>
+<p>Das Spielbrett besteht ebenfalls aus Dreiecken und hat die Form eines
+Sechsecks mit jeweils neun Dreiecken pro Kante.</p>
+<p align="center"><img src="../../C/pentobi/board_trigon.jpg" alt=
+"Spielbrett für Spielvariante Trigon"></p>
+<div align="center" class="caption">Das Brett mit den Startfeldern<br>
+durch graue Punkte markiert.</div>
+<p>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.</p>
+<p align="center"><img src="../../C/pentobi/position_trigon.jpg" alt=
+"Beispielstellung für Spielvariante Trigon"></p>
+<div align="center" class="caption">Eine Beispielstellung nach ein paar
+Zügen.</div>
+<h3>Regeln für zwei Spieler</h3>
+<p>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.</p>
+<h3>Regeln für drei Spieler</h3>
+<p>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.</p>
+<p align="right"><a href="duo_rules.html">Zurück</a> | <a href=
+"junior_rules.html">Weiter</a></p>
+</body>
+</html>
diff --git a/src/pentobi/help/de/pentobi/user_interface.html b/src/pentobi/help/de/pentobi/user_interface.html
new file mode 100644 (file)
index 0000000..6a5dc95
--- /dev/null
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="de">
+<head>
+<title>Pentobi-Hilfe</title>
+<link rel="stylesheet" type="text/css" href="../../C/pentobi/stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="callisto_rules.html">Zurück</a> | <a href=
+"become_stronger.html">Weiter</a></p>
+<h2>Wie Sie Pentobi benutzen</h2>
+<h3>Spielbrett</h3>
+<p>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).</p>
+<p>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.</p>
+<h3>Spielsteine und Punkte</h3>
+<p>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.</p>
+<p>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 <a href=
+"shortcuts.html">Tastenkürzeln</a>.</p>
+<p>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.</p>
+<h3>Gegen den Computer spielen</h3>
+<p>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.</p>
+<p>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 <i>Computer-Farben</i> aus dem Menü
+<i>Computer</i> oder der Werkzeugleiste und wählen Sie die Farben, die der
+Computer spielen soll.</p>
+<p>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 <i>Computer-Farben</i> abschalten und diese
+Einstellung wird sich nicht ändern. Nach dem Laden eines Spiels ist ebenfalls
+voreingestellt, dass der Computer keine Farbe spielt.</p>
+<p>Die Auswahl von <i>Spielen</i> aus dem Menü <i>Computer</i> 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.</p>
+<h3>Zugvarianten und der Spielbaum</h3>
+<p>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 <i>Gehe zu</i> oder der Werkzeugleiste navigieren.</p>
+<p>Die Hauptvariante ist die Zugfolge, die in der Startstellung beginnt und
+immer den ersten Kindknoten in jeder Brettstellung wählt (z.&nbsp;B. indem Sie
+<i>Vorwärts</i> im Menü <i>Gehe zu</i> oder der Werkzeugleiste benutzen). Die
+Hauptvariante sollte das wirklich gespielte Spiel darstellen. Wenn Sie eine
+Nebenvariante zur Hauptvariante machen wollen, wählen Sie <i>Zu Hauptvariante
+machen</i> aus dem <i>Bearbeiten</i>-Menü.</p>
+<p align="right"><a href="callistorules.html">Zurück</a> | <a href=
+"become_stronger.html">Weiter</a></p>
+</body>
+</html>
diff --git a/src/pentobi/help/de/pentobi/window_menu.html b/src/pentobi/help/de/pentobi/window_menu.html
new file mode 100644 (file)
index 0000000..e861dbe
--- /dev/null
@@ -0,0 +1,226 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="de">
+<head>
+<title>Pentobi-Hilfe</title>
+<link rel="stylesheet" type="text/css" href="../../C/pentobi/stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="become_stronger.html">Zurück</a> | <a href=
+"shortcuts.html">Weiter</a></p>
+<h2>Das Fenstermenü</h2>
+<h3>Spiel</h3>
+<dl>
+<dt>Neu</dt>
+<dd>Beginnt ein neues Spiel.</dd>
+<dt>Gewertetes Spiel</dt>
+<dd>Beginnt ein neues <a href="become_stronger.html#rating">gewertetes
+Spiel</a> gegen den Computer.</dd>
+<dt>Spielvariante</dt>
+<dd>Wählt eine Spielvariante und beginnt ein neues Spiel dieser
+Spielvariante.</dd>
+<dt>Spielinformation</dt>
+<dd>Ö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.</dd>
+<dt>Zug rückgängig</dt>
+<dd>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.&nbsp;h. ein Endknoten im Spielbaum; benutzen Sie
+<i>Bearbeiten/Abschneiden</i> zum Entfernen innerer Knoten aus dem
+Spielbaum).</dd>
+<dt>Zug finden</dt>
+<dd>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.</dd>
+<dt>Öffnen</dt>
+<dd>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.</dd>
+<dt>Zuletzt benutzte Dateien</dt>
+<dd>Lädt ein kürzlich benutztes Spiel.</dd>
+<dt>Speichern</dt>
+<dd>Speichert das gegenwärtige Spiel.</dd>
+<dt>Speichern unter</dt>
+<dd>Speichert das gegenwärtige Spiel unter einem neuen Dateinamen.</dd>
+<dt>Exportieren/Grafik</dt>
+<dd>Speichert die gegenwärtige Brettstellung als eine Grafikdatei. Mehrere
+Grafikdateiformate werden unterstützt, das Dateiformat wird von der Dateiendung
+abgeleitet (z.&nbsp;B. „.png“ für das PNG-Format).</dd>
+<dt>Exportieren/ASCII-Art</dt>
+<dd>Speichert die gegenwärtige Brettstellung als Textdiagramm. Das Textdiagramm
+sollte mit einer Schriftart fester Breite betrachtet werden.</dd>
+<dt>Beenden</dt>
+<dd>Beendet Pentobi.</dd>
+</dl>
+<h3>Gehe zu</h3>
+<dl>
+<dt>Anfang</dt>
+<dd>Geht zum Anfang des Spiels.</dd>
+<dt>Zurück</dt>
+<dd>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.</dd>
+<dt>Vorwärts</dt>
+<dd>Geht einen Zug in der gegenwärtigen Variante vorwärts. Wenn die
+gegenwärtige Brettstellung mehrerer nachfolgende Varianten hat (d.&nbsp;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.</dd>
+<dt>Ende</dt>
+<dd>Geht zum Ende der gegenwärtigen Variante. Wie bei <i>Vorwärts</i> wird auch
+hier jeweils die erste Variante benutzt, wenn die Brettstellung mehrere
+nachfolgende Varianten hat.</dd>
+<dt>Nächste Variante</dt>
+<dd>Geht zur nächsten Variante zum zuletzt gespielten Zug (d.&nbsp;h. zum
+nächsten Geschwisterknoten des gegenwärtigen Knotens im Spielbaum).</dd>
+<dt>Vorherige Variante</dt>
+<dd>Geht zur vorherigen Variante zum zuletzt gespielten Zug (d.&nbsp;h. zum
+vorherigen Geschwisterknoten des gegenwärtigen Knotens im Spielbaum).</dd>
+<dt>Gehe zu Zug</dt>
+<dd>Geht zum Zug mit einer bestimmten Nummer in der gegenwärtigen
+Variante.</dd>
+<dt>Zurück zu Hauptvariante</dt>
+<dd>Kehrt zur letzten Brettstellung in der gegenwärtigen Variante zurück, die
+zur Hauptvariante gehörte.</dd>
+<dt>Anfang der Verzweigung</dt>
+<dd>Kehrt zur letzten Brettstellung in der gegenwärtigen Variante zurück, die
+einen alternativen Zug hatte.</dd>
+<dt>Nächsten Kommentar finden</dt>
+<dd>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.</dd>
+</dl>
+<h3>Bearbeiten</h3>
+<dl>
+<dt>Zugkommentierung</dt>
+<dd>Fügt ein wie in der Schachnotation benutztes Symbol (z.&nbsp;B. !!) zum
+gegenwärtigen Zug hinzu. Die Symbole werden an die Zugnummern in der
+Statusleiste angehängt und, abhängig von der Einstellung von
+<i>Zugmarkierung</i>, an die auf dem Spielbrett.</dd>
+<dt>Zu Hauptvariante machen</dt>
+<dd>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.</dd>
+<dt>Variante nach oben schieben</dt>
+<dd>Ändert die Reihenfolge der Varianten so, dass die gegenwärtige
+Brettstellung beim Durchlaufen der Varianten mit <i>Nächste/Vorherige
+Variante</i> früher erscheint.</dd>
+<dt>Variante nach unten schieben</dt>
+<dd>Ändert die Reihenfolge der Varianten so, dass die gegenwärtige
+Brettstellung beim Durchlaufen der Varianten mit <i>Nächste/Vorherige
+Variante</i> später erscheint.</dd>
+<dt>Alle Varianten löschen</dt>
+<dd>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 <i>Zurück zu
+Hauptvariante</i>.</dd>
+<dt>Abschneiden</dt>
+<dd>Entfernt den Knoten mit der gegenwärtigen Brettstellung zusammen mit dem
+auf ihn folgenden Teilbaum aus dem Spielbaum.</dd>
+<dt>Kindknoten abschneiden</dt>
+<dd>Entfernt alle Kindknoten des Knotens mit der gegenwärtigen Brettstellung
+aus dem Spielbaum.</dd>
+<dt>Nur Brettstellung behalten</dt>
+<dd>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.</dd>
+<dt>Nur Teilbaum behalten</dt>
+<dd>Wie <i>Nur Brettstellung behalten</i>, aber die Züge nach der gegenwärtigen
+Brettstellung werden nicht gelöscht.</dd>
+<dt>Stellungsaufbau</dt>
+<dd>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
+<i>Nächste Farbe</i> 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.</dd>
+<dt>Nächste Farbe</dt>
+<dd>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.</dd>
+</dl>
+<h3>Ansicht</h3>
+<dl>
+<dt>Werkzeugleiste</dt>
+<dd>Zeigt oder verbirgt die Werkzeugleiste.</dd>
+<dt>Werkzeugleistentext</dt>
+<dd>Konfiguriert das Aussehen der Werkzeugleiste.</dd>
+<dt>Kommentar</dt>
+<dd>Zeigt oder verbirgt ein Textfeld zum Anzeigen oder Bearbeiten von
+Kommentaren zur gegenwärtigen Brettstellung.</dd>
+<dt>Zugnummern</dt>
+<dd>Ä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.</dd>
+<dt>Zugmarkierung</dt>
+<dd>Ä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.</dd>
+<dt>Koordinaten</dt>
+<dd>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.</dd>
+<dt>Varianten zeigen</dt>
+<dd>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.</dd>
+<dt>Vollbild</dt>
+<dd>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.</dd>
+</dl>
+<h3>Computer</h3>
+<dl>
+<dt>Computer-Farben</dt>
+<dd>Wählt aus, welche Farben vom Computer gespielt werden.</dd>
+<dt>Spielen</dt>
+<dd>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.</dd>
+<dt>Einzelnen Zug spielen</dt>
+<dd>Lässt den Computer einen einzelnen Zug für die gegenwärtige Farbe spielen
+ohne die vom Computer gespielten Farben zu ändern.</dd>
+<dt>Stopp</dt>
+<dd>Bricht die gegenwärtige Zuggenerierung ab. Sie können den Computer
+weiterspielen lassen, indem Sie <i>Spielen</i> auswählen.</dd>
+<dt>Spielstufe</dt>
+<dd>Ä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.</dd>
+</dl>
+<h3>Extras</h3>
+<dl>
+<dt>Wertung</dt>
+<dd>Zeigt ein Dialogfenster mit der <a href=
+"become_stronger.html#rating">Wertung</a> des Benutzers in der gegenwärtigen
+Spielvariante.</dd>
+<dt>Spiel analysieren</dt>
+<dd>Führt eine <a href="become_stronger.html#analysis">Spielanalyse</a>
+durch.</dd>
+</dl>
+<h3>Hilfe</h3>
+<dl>
+<dt>Pentobi-Hilfe</dt>
+<dd>Zeigt ein Fenster mit dem Pentobi-Benutzerhandbuch.</dd>
+<dt>Über Pentobi</dt>
+<dd>Zeigt eine Dialogfenster mit Informationen über diese Version von
+Pentobi.</dd>
+</dl>
+<p align="right"><a href="become_stronger.html">Zurück</a> | <a href=
+"shortcuts.html">Weiter</a></p>
+</body>
+</html>
diff --git a/src/pentobi/icons.qrc b/src/pentobi/icons.qrc
new file mode 100644 (file)
index 0000000..735f5c8
--- /dev/null
@@ -0,0 +1,15 @@
+<!-- Subset of the icons in pentobi/icons for use in pentobi_qml -->
+<RCC>
+    <qresource prefix="/qml">
+        <file>icons/pentobi-backward.svg</file>
+        <file>icons/pentobi-beginning.svg</file>
+        <file>icons/pentobi-computer-colors.svg</file>
+        <file>icons/pentobi-end.svg</file>
+        <file>icons/pentobi-forward.svg</file>
+        <file>icons/pentobi-newgame.svg</file>
+        <file>icons/pentobi-next-variation.svg</file>
+        <file>icons/pentobi-play.svg</file>
+        <file>icons/pentobi-previous-variation.svg</file>
+        <file>icons/pentobi-undo.svg</file>
+    </qresource>
+</RCC>
diff --git a/src/pentobi/icons/pentobi-16.svg b/src/pentobi/icons/pentobi-16.svg
new file mode 100644 (file)
index 0000000..9688842
--- /dev/null
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <g id="a">
+  <rect width="4" height="4" x="8" fill="#73d216"/>
+  <path d="m8 4v-4h4l-1 1h-2v2z" fill="#b8e888"/>
+ </g>
+ <use x="4" xlink:href="#a"/>
+ <use y="4" x="4" xlink:href="#a"/>
+ <use y="8" x="4" xlink:href="#a"/>
+ <g id="b">
+  <rect height="4" width="4" y="8" x="4" fill="#3465a4"/>
+  <path d="m4 12v-4h4l-1 1h-2v2z" fill="#82a0c7"/>
+ </g>
+ <use x="4" xlink:href="#b"/>
+ <use y="4" x="4" xlink:href="#b"/>
+ <use y="4" x="8" xlink:href="#b"/>
+ <g id="c">
+  <rect y="4" width="4" height="4" fill="#edd400"/>
+  <path d="m0 8v-4h4l-1 1h-2v2z" fill="#fff283"/>
+ </g>
+ <use y="4" xlink:href="#c"/>
+ <use y="8" xlink:href="#c"/>
+ <use y="8" x="4" xlink:href="#c"/>
+ <g id="d">
+  <rect width="4" height="4" fill="#C00"/>
+  <path d="m0 4v-4h4l-1 1h-2v2z" fill="#f57b7b"/>
+ </g>
+ <use x="4" xlink:href="#d"/>
+ <use y="4" x="4" xlink:href="#d"/>
+ <use y="4" x="8" xlink:href="#d"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-32.svg b/src/pentobi/icons/pentobi-32.svg
new file mode 100644 (file)
index 0000000..387a5b4
--- /dev/null
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="32" width="32" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<g id="a">
+<rect height="7" width="7" x="16" y="2" fill="#73d216"/>
+<path d="m16 9h7v-7l-1 1v 5h-5z" fill="#4e9a06"/>
+<path d="m16 9v-7h7l-1 1h-5v5z" fill="#8ae234"/>
+</g>
+<use xlink:href="#a" x="7"/>
+<use xlink:href="#a" x="7" y="7"/>
+<use xlink:href="#a" x="7" y="14"/>
+<g id="b">
+<rect height="7" width="7" x="9" y="16" fill="#3465a4"/>
+<path d="m9 23h7v-7l-1 1v 5h-5z" fill="#204a87"/>
+<path d="m9 23v-7h7l-1 1h-5v5z" fill="#558bc5"/>
+</g>
+<use xlink:href="#b" x="7"/>
+<use xlink:href="#b" x="7" y="7"/>
+<use xlink:href="#b" x="14" y="7"/>
+<g id="c">
+<rect height="7" width="7" x="2" y="9" fill="#edd400"/>
+<path d="m2 16h7v-7l-1 1v 5h-5z" fill="#c4a000"/>
+<path d="m2 16v-7h7l-1 1h-5v5z" fill="#fce94f"/>
+</g>
+<use xlink:href="#c" y="7"/>
+<use xlink:href="#c" y="14"/>
+<use xlink:href="#c" x="7" y="14"/>
+<g id="d">
+<rect height="7" width="7" x="2" y="2" fill="#C00"/>
+<path d="m2 9h7v-7l-1 1v 5h-5z" fill="#a40000"/>
+<path d="m2 9v-7h7l-1 1h-5v5z" fill="#ef2929"/>
+</g>
+<use xlink:href="#d" x="7"/>
+<use xlink:href="#d" x="7" y="7"/>
+<use xlink:href="#d" x="14" y="7"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-64.svg b/src/pentobi/icons/pentobi-64.svg
new file mode 100644 (file)
index 0000000..4f371cd
--- /dev/null
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="64" width="64" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<g id="a">
+<rect height="14" width="14" x="32" y="4" fill="#73d216"/>
+<path d="m32 18h14v-14l-1 1v12h-12z" fill="#4e9a06"/>
+<path d="m32 18v-14h14l-1 1h-12v12z" fill="#8ae234"/>
+</g>
+<use xlink:href="#a" x="14"/>
+<use xlink:href="#a" x="14" y="14"/>
+<use xlink:href="#a" x="14" y="28"/>
+<g id="b">
+<rect height="14" width="14" x="18" y="32" fill="#3465a4"/>
+<path d="m18 46h14v-14l-1 1v12h-12z" fill="#204a87"/>
+<path d="m18 46v-14h14l-1 1h-12v12z" fill="#558bc5"/>
+</g>
+<use xlink:href="#b" x="14"/>
+<use xlink:href="#b" x="14" y="14"/>
+<use xlink:href="#b" x="28" y="14"/>
+<g id="c">
+<rect height="14" width="14" x="4" y="18" fill="#edd400"/>
+<path d="m4 32h14v-14l-1 1v12h-12z" fill="#c4a000"/>
+<path d="m4 32v-14h14l-1 1h-12v12z" fill="#fce94f"/>
+</g>
+<use xlink:href="#c" y="14"/>
+<use xlink:href="#c" y="28"/>
+<use xlink:href="#c" x="14" y="28"/>
+<g id="d">
+<rect height="14" width="14" x="4" y="4" fill="#C00"/>
+<path d="m4 18h14v-14l-1 1v12h-12z" fill="#a40000"/>
+<path d="m4 18v-14h14l-1 1h-12v12z" fill="#ef2929"/>
+</g>
+<use xlink:href="#d" x="14"/>
+<use xlink:href="#d" x="14" y="14"/>
+<use xlink:href="#d" x="28" y="14"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-backward-16.svg b/src/pentobi/icons/pentobi-backward-16.svg
new file mode 100644 (file)
index 0000000..42a4540
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m6.75 3.5v1.75h7v5.5h-7v1.75l-4.95-4.5z" stroke="#babdb6" stroke-width="1.5" fill="#888a85"/>
+<path stroke-linejoin="round" d="m7.5 1.5v3h7v7h-7v3l-7-6.5z" stroke="#555753" fill="none"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-backward.svg b/src/pentobi/icons/pentobi-backward.svg
new file mode 100644 (file)
index 0000000..7fbe3eb
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m9.75 4.5v2.75h10v7.5h-10v2.75l-6.5-6.5z" stroke="#babdb6" stroke-width="1.5" fill="#888a85"/>
+<path stroke-linejoin="round" d="m10.5 2.5v4h10v9h-10v4l-8.5-8.5z" stroke="#555753" fill="none"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-beginning-16.svg b/src/pentobi/icons/pentobi-beginning-16.svg
new file mode 100644 (file)
index 0000000..68f4afb
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <path d="m3 2v12h-1.5v-12zm5.75 1.4v1.85h6v5.5h-6v1.95l-5.3-4.8z" stroke="#babdb6" stroke-width="1.5" fill="#888a85"/>
+ <path stroke-linejoin="round" stroke="#555753" d="m3.5 1.5v5l6-5v3h6v7h-6v3l-6-5v5h-3v-13z" fill="none"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-beginning.svg b/src/pentobi/icons/pentobi-beginning.svg
new file mode 100644 (file)
index 0000000..9e754ac
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <path d="m3.75 3.25v15.5h-2.5v-15.5zm8 1.25v2.75h9v7.5h-9v2.75l-6.5-6.5z" stroke="#babdb6" stroke-width="1.5" fill="#888a85"/>
+ <path stroke-linejoin="round" d="m4.5 2.5v8l8-8v4h9v9h-9v4l-8-8v8h-4v-17z" stroke="#555753" fill="none"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-computer-colors-16.svg b/src/pentobi/icons/pentobi-computer-colors-16.svg
new file mode 100644 (file)
index 0000000..aa6f6ac
--- /dev/null
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <defs>
+  <linearGradient id="a" y2="20" gradientUnits="userSpaceOnUse" y1="0" x2="15" x1="10">
+   <stop stop-color="#dddfe2" offset="0"/>
+   <stop offset="1"/>
+  </linearGradient>
+ </defs>
+ <rect stroke-linejoin="round" ry="1.7" height="14" width="15" stroke="#555753" y=".5" x=".5" fill="#dfe2dc"/>
+ <rect stroke-linejoin="round" ry=".34" height="8" width="11" stroke="#2e3436" y="2.5" x="2.5" fill="url(#a)"/>
+ <path stroke="#555753" d="m2 12.5h12" fill="none"/>
+ <path d="m13 4v6h-10v-2.6789c2.8021-1.303 6.4697-2.752 10-3.321z" fill-opacity=".2"/>
+ <rect stroke-linejoin="round" height="9" width="9" stroke="#2b2d2b" y="6.5" x="6.5" fill="none"/>
+ <rect height="4" width="4" y="7" x="7" fill="#e63232"/>
+ <rect height="4" width="4" y="7" x="11" fill="#8ad83e"/>
+ <rect height="4" width="4" y="11" x="7" fill="#e3cc09"/>
+ <rect height="4" width="4" y="11" x="11" fill="#4b7bb9"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-computer-colors.svg b/src/pentobi/icons/pentobi-computer-colors.svg
new file mode 100644 (file)
index 0000000..c9870f0
--- /dev/null
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <defs>
+  <linearGradient id="a" y2="20" gradientUnits="userSpaceOnUse" y1="0" x2="15" x1="10">
+   <stop stop-color="#dddfe2" offset="0"/>
+   <stop offset="1"/>
+  </linearGradient>
+ </defs>
+ <rect stroke-linejoin="round" ry="2.3" height="19" width="21" stroke="#555753" y=".5" x=".5" fill="#dfe2dc"/>
+ <rect stroke-linejoin="round" ry=".5" height="12" width="15" stroke="#2e3436" y="3.5" x="3.5" fill="url(#a)"/>
+ <path stroke="#555753" d="m3 17.5h16" fill="none"/>
+ <path d="m18 6.5v8.5h-14v-4c4-2 9-4 14-4.75z" fill-opacity=".2"/>
+ <rect stroke-linejoin="round" height="11" width="11" stroke="#2b2d2b" y="10.5" x="10.5" fill="none"/>
+ <rect height="5" width="5" y="11" x="11" fill="#e63232"/>
+ <rect height="5" width="5" y="11" x="16" fill="#8ad83e"/>
+ <rect height="5" width="5" y="16" x="11" fill="#e3cc09"/>
+ <rect height="5" width="5" y="16" x="16" fill="#4b7bb9"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-end-16.svg b/src/pentobi/icons/pentobi-end-16.svg
new file mode 100644 (file)
index 0000000..d249048
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <path d="m13 2v12h1.5v-12zm-5.75 1.4v1.85h-6v5.5h6v1.95l5.3-4.8z" stroke="#babdb6" stroke-width="1.5" fill="#888a85"/>
+ <path stroke-linejoin="round" stroke="#555753" d="m12.5 1.5v5l-6-5v3h-6v7h6v3l6-5v5h3v-13z" fill="none"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-end.svg b/src/pentobi/icons/pentobi-end.svg
new file mode 100644 (file)
index 0000000..f657d97
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m18.25 3.25v15.5h2.5v-15.5zm-8 1.25v2.75h-9v7.5h9v2.75l6.5-6.5z" stroke="#babdb6" stroke-width="1.5" fill="#888a85"/>
+<path stroke-linejoin="round" d="m17.5 2.5v8l-8-8v4h-9v9h9v4l8-8v8h4v-17z" stroke="#555753" fill="none"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-flip-horizontal.svg b/src/pentobi/icons/pentobi-flip-horizontal.svg
new file mode 100644 (file)
index 0000000..193ba08
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<defs>
+</defs>
+<path stroke-linejoin="round" d="m5.5 3-5 5 5 5v-3.5h5v3.5l5-5-5-5v3.5h-5z" stroke="#555" fill="#babdb6"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-flip-vertical.svg b/src/pentobi/icons/pentobi-flip-vertical.svg
new file mode 100644 (file)
index 0000000..1d266c3
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<defs>
+</defs>
+<path stroke-linejoin="round" d="m13 5.5-5-5-5 5h3.5v5h-3.5l5 5 5-5h-3.5v-5z" stroke="#555" fill="#babdb6"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-forward-16.svg b/src/pentobi/icons/pentobi-forward-16.svg
new file mode 100644 (file)
index 0000000..ebce7fd
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m9.25 3.5v1.75h-7v5.5h7v1.75l4.95-4.5z" stroke="#babdb6" stroke-width="1.5" fill="#888a85"/>
+<path stroke-linejoin="round" d="m8.5 1.5v3h-7v7h7v3l7-6.5z" stroke="#555753" fill="none"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-forward.svg b/src/pentobi/icons/pentobi-forward.svg
new file mode 100644 (file)
index 0000000..8957c14
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m12.25 4.5v2.75h-10v7.5h10v2.75l6.5-6.5z" stroke="#babdb6" stroke-width="1.5" fill="#888a85"/>
+<path stroke-linejoin="round" d="m11.5 2.5v4h-10v9h10v4l8.5-8.5z" stroke="#555753" fill="none"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-newgame-16.svg b/src/pentobi/icons/pentobi-newgame-16.svg
new file mode 100644 (file)
index 0000000..17df58f
--- /dev/null
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <rect height="12" width="12" y="2" x="2" fill="#babdb6"/>
+ <g id="a">
+  <path d="m1 8v-7h7l-1 1h-5v5z" fill="#999B96"/>
+  <path d="m1 8h7v-7l-1 1v5h-5" fill="#c8cdc3"/>
+ </g>
+ <use xlink:href="#a" transform="translate(7)"/>
+ <use xlink:href="#a" transform="translate(0 7)"/>
+ <use xlink:href="#a" transform="translate(7 7)"/>
+ <rect stroke-linejoin="round" ry="1.5" height="15" width="15" stroke="#878A84" y=".5" x=".5" fill="none"/>
+ <path d="m11 .51c0.54 0 1 2 1.4 2.3 .44 .3 2.8 .16 2.9 .63 .18 .46-1.8 1.6-2 2-.17 .48 .7 2.4 .25 2.8-.43 .3-2.1-1.1-2.7-1.1-.55 0-2.3 1.3-2.8 1.1s0.4-2.2 .2-2.6c-.1-.4-2.1-1.6-2-2.1 .2-.4 2.4-.3 2.9-.5 .5-.4 .8-2.5 1.8-2.5z" stroke="#edd400" fill="#fff"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-newgame.svg b/src/pentobi/icons/pentobi-newgame.svg
new file mode 100644 (file)
index 0000000..1a2c884
--- /dev/null
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <rect height="18" width="18" y="2" x="2" fill="#babdb6"/>
+ <g id="a">
+  <path d="m1 11v-10h10l-1 1h-8v8z" fill="#999B96"/>
+  <path d="m1 11h10v-10l-1 1v8h-8" fill="#c8cdc3"/>
+ </g>
+ <use xlink:href="#a" transform="translate(10)"/>
+ <use xlink:href="#a" transform="translate(0 10)"/>
+ <use xlink:href="#a" transform="translate(10 10)"/>
+ <rect stroke-linejoin="round" ry="1.5" height="21" width="21" stroke="#878A84" y=".5" x=".5" fill="none"/>
+ <path stroke="#edd400" d="m16 .64c.69 .02 1.4 2.6 2 3 .53 .36 3.3 .11 3.5 .7 .19 .62-2.2 2.1-2.5 2.7-.2 .58 .91 3 .37 3.3-.57 .36-2.8-1.4-3.5-1.4-.66 0 -2.8 1.7-3.3 1.4-.55-.4 .52-2.9 .3-3.5-.2-.58-2.6-1.9-2.4-2.5 .24-.61 3.1-.43 3.7-.81 .53-.36 1.1-2.9 1.8-2.9z" fill="#fff"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-next-piece.svg b/src/pentobi/icons/pentobi-next-piece.svg
new file mode 100644 (file)
index 0000000..f3d844d
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<defs>
+</defs>
+<path stroke-linejoin="round" d="m9.5 13 5-5l-5-5v3.5h-9v3h9z" stroke="#555" fill="#babdb6"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-next-variation-16.svg b/src/pentobi/icons/pentobi-next-variation-16.svg
new file mode 100644 (file)
index 0000000..72a9727
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m3.5 9.25h1.75v-7h5.5v7h1.75l-4.5 4.95z" stroke="#babdb6" stroke-width="1.5" fill="#888a85"/>
+<path stroke-linejoin="round" d="m1.5 8.5h3v-7h7v7h3l-6.5 7z" stroke="#555753" fill="none"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-next-variation.svg b/src/pentobi/icons/pentobi-next-variation.svg
new file mode 100644 (file)
index 0000000..cc39628
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m4.5 13.25h2.75v-10h7.5v10h2.75l-6.5 6.5z" stroke="#babdb6" stroke-width="1.5" fill="#888a85"/>
+<path stroke-linejoin="round" d="m2.5 12.5h4v-10h9v10h4l-8.5 8.5z" stroke="#555753" fill="none"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-piece-clear.svg b/src/pentobi/icons/pentobi-piece-clear.svg
new file mode 100644 (file)
index 0000000..f8cdbd4
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<defs>
+</defs>
+<path stroke-linejoin="round" d="m3.5 1.5-2 2 4 4-4 4 2 2 4-4 4 4 2-2-4-4 4-4-2-2-4 4-4-4z" stroke="#555" fill="#babdb6"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-play-16.svg b/src/pentobi/icons/pentobi-play-16.svg
new file mode 100644 (file)
index 0000000..8d09c1c
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <defs>
+  <linearGradient id="a" gradientUnits="userSpaceOnUse" x2="15" x1="10" y2="20">
+   <stop stop-color="#dddfe2" offset="0"/>
+   <stop offset="1"/>
+  </linearGradient>
+ </defs>
+ <rect stroke-linejoin="round" ry="2.3" height="19" width="21" stroke="#555753" y=".5" x=".5" fill="#dfe2dc"/>
+ <rect stroke-linejoin="round" ry=".5" height="12" width="15" stroke="#2e3436" y="3.5" x="3.5" fill="url(#a)"/>
+ <path stroke="#555753" d="m3 17.5h16" fill="none"/>
+ <path d="m18 6.5v8.5h-14v-4c4-2 9-4 14-4.75z" fill-opacity=".2"/>
+ <path stroke-linejoin="round" d="m21.5 14.5-11 7v-13.5z" stroke="#2e3436" fill="#eeeeec"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-play.svg b/src/pentobi/icons/pentobi-play.svg
new file mode 100644 (file)
index 0000000..d860369
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <defs>
+  <linearGradient id="a" y2="20" gradientUnits="userSpaceOnUse" x2="15" x1="10">
+   <stop stop-color="#dddfe2" offset="0"/>
+   <stop offset="1"/>
+  </linearGradient>
+ </defs>
+ <rect stroke-linejoin="round" ry="2.31" height="19" width="21" stroke="#555753" y=".5" x=".5" fill="#dfe2dc"/>
+ <rect stroke-linejoin="round" ry=".5" height="12" width="15" stroke="#2e3436" y="3.5" x="3.5" fill="url(#a)"/>
+ <path stroke="#555753" d="m3 17.5h16" fill="none"/>
+ <path d="m18 6.5v8.5h-14v-4c4-2 9-4 14-4.75z" fill-opacity=".2"/>
+ <path stroke-linejoin="round" d="m21.5 15-9 6.5v-13z" stroke="#2e3436" fill="#eeeeec"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-previous-piece.svg b/src/pentobi/icons/pentobi-previous-piece.svg
new file mode 100644 (file)
index 0000000..1180935
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<defs>
+</defs>
+<path stroke-linejoin="round" d="m5.5 3-5 5 5 5v-3.5h9v-3h-9z" stroke="#555" fill="#babdb6"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-previous-variation-16.svg b/src/pentobi/icons/pentobi-previous-variation-16.svg
new file mode 100644 (file)
index 0000000..f98e8a0
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m3.5 7.75h1.75v7h5.5v-7h1.75l-4.5-4.95z" stroke="#babdb6" stroke-width="1.5" fill="#888a85"/>
+<path stroke-linejoin="round" d="m1.5 8.5h3v7h7v-7h3l-6.5 -7z" stroke="#555753" fill="none"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-previous-variation.svg b/src/pentobi/icons/pentobi-previous-variation.svg
new file mode 100644 (file)
index 0000000..2d1d3f8
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m4.5 9.75h2.75v10h7.5v-10h2.75l-6.5-6.5z" stroke="#babdb6" stroke-width="1.5" fill="#888a85"/>
+<path stroke-linejoin="round" d="m2.5 10.5h4v10h9v-10h4l-8.5-8.5z" stroke="#555753" fill="none"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-rated-game-16.svg b/src/pentobi/icons/pentobi-rated-game-16.svg
new file mode 100644 (file)
index 0000000..6bfe814
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <rect x="4" y=".5" width="8" height="2" fill="#9b9f95" stroke="#878A84"/>
+ <path d="m6 13-5-11 .5-1.5h2.5l6 12z" fill="#babdb6" stroke="#878A84" stroke-linejoin="round"/>
+ <path d="m10 13 5-11-.5-1.5h-2.5l-6 12z" fill="#babdb6" stroke="#878A84" stroke-linejoin="round"/>
+ <circle cy="11" cx="8" r="5" fill="#a08300"/>
+ <circle stroke="#edd400" cy="11" cx="8" r="3.5" fill="#fce94f"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-rated-game.svg b/src/pentobi/icons/pentobi-rated-game.svg
new file mode 100644 (file)
index 0000000..8a714cf
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <rect x="5" y=".5" width="12" height="3" fill="#9b9f95" stroke="#878A84"/>
+ <path d="m6 13-5-11 .5-1.5h3.5l6 12z" fill="#babdb6" stroke="#878A84" stroke-linejoin="round"/>
+ <path d="m16 13 5-11-.5-1.5h-3.5l-6 12z" fill="#babdb6" stroke="#878A84" stroke-linejoin="round"/>
+ <circle cy="15" stroke="#a08300" cx="11" r="6.5" fill="#edd400"/>
+ <circle cy="15" stroke="#bf9c00" cx="11" r="4.5" fill="#fce94f"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-rotate-left.svg b/src/pentobi/icons/pentobi-rotate-left.svg
new file mode 100644 (file)
index 0000000..8043152
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <path stroke-linejoin="round" stroke="#555" d="m7.5 .47c3.9 0 7 3.1 7 7h-3c0-2.2-1.8-4-4-4s-4 1.8-4 4c0 1.1 .48 2.2 1.2 2.9l1.9-1.9v6h-6l2-2c-1.3-1.3-2.1-3.1-2.1-5 0-3.9 3.1-7 7-7z" fill="#babdb6"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-rotate-right.svg b/src/pentobi/icons/pentobi-rotate-right.svg
new file mode 100644 (file)
index 0000000..72aa153
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <path stroke-linejoin="round" stroke="#555" d="m7.5 .47c-3.9 0-7 3.1-7 7h3c0-2.2 1.8-4 4-4s4 1.8 4 4c0 1.1-.48 2.2-1.2 2.9l-1.9-1.9v6h6l-2-2c1.3-1.3 2.1-3.1 2.1-5 0-3.9-3.1-7-7-7z" fill="#babdb6"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-undo-16.svg b/src/pentobi/icons/pentobi-undo-16.svg
new file mode 100644 (file)
index 0000000..991c9d4
--- /dev/null
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <rect height="12" width="12" y="2" x="2" fill="#babdb6"/>
+ <g id="a">
+  <path d="m1 8v-7h7l-1 1h-5v5z" fill="#999B96"/>
+  <path d="m1 8h7v-7l-1 1v5h-5" fill="#c8cdc3"/>
+ </g>
+ <use xlink:href="#a" transform="translate(7)"/>
+ <use xlink:href="#a" transform="translate(0 7)"/>
+ <use xlink:href="#a" transform="translate(7 7)"/>
+ <rect stroke-linejoin="round" ry="1.5" height="15" width="15" stroke="#878A84" y=".5" x=".5" fill="none"/>
+ <path stroke="#204a87" d="m1.5 9 5-5v3.6s2.1 0 3.5 1.4c1.1 1.1 1.5 4.3 1.5 4.3s-1.4-1.6-2.3-2.2c-1.2-.77-2.7-.64-2.7-.64v3.6z" fill="#729fcf"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi-undo.svg b/src/pentobi/icons/pentobi-undo.svg
new file mode 100644 (file)
index 0000000..45bc84e
--- /dev/null
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <rect height="18" width="18" y="2" x="2" fill="#babdb6"/>
+ <g id="a">
+  <path d="m1 11v-10h10l-1 1h-8v8z" fill="#999B96"/>
+  <path d="m1 11h10v-10l-1 1v8h-8" fill="#c8cdc3"/>
+ </g>
+ <use xlink:href="#a" transform="translate(10)"/>
+ <use xlink:href="#a" transform="translate(0 10)"/>
+ <use xlink:href="#a" transform="translate(10 10)"/>
+ <rect stroke-linejoin="round" ry="1.5" height="21" width="21" stroke="#878A84" y=".5" x=".5" fill="none"/>
+ <path stroke="#204a87" d="m1.5 13 7-7v5s3 0 4.9 2c1.5 1.5 2.1 6 2.1 6s-2-2.3-3.3-3.1c-1.7-1.1-3.7-.89-3.7-.89v5z" fill="#729fcf"/>
+</svg>
diff --git a/src/pentobi/icons/pentobi.svg b/src/pentobi/icons/pentobi.svg
new file mode 100644 (file)
index 0000000..fea7ae6
--- /dev/null
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="48" width="48" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<g id="a">
+<rect height="10" width="10" x="24" y="4" fill="#73d216"/>
+<path d="m24 14h10v-10l-1 1v 8h-8z" fill="#4e9a06"/>
+<path d="m24 14v-10h10l-1 1h-8v8z" fill="#8ae234"/>
+</g>
+<use xlink:href="#a" x="10"/>
+<use xlink:href="#a" x="10" y="10"/>
+<use xlink:href="#a" x="10" y="20"/>
+<g id="b">
+<rect height="10" width="10" x="14" y="24" fill="#3465a4"/>
+<path d="m14 34h10v-10l-1 1v 8h-8z" fill="#204a87"/>
+<path d="m14 34v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+</g>
+<use xlink:href="#b" x="10"/>
+<use xlink:href="#b" x="10" y="10"/>
+<use xlink:href="#b" x="20" y="10"/>
+<g id="c">
+<rect height="10" width="10" x="4" y="14" fill="#edd400"/>
+<path d="m4 24h10v-10l-1 1v 8h-8z" fill="#c4a000"/>
+<path d="m4 24v-10h10l-1 1h-8v8z" fill="#fce94f"/>
+</g>
+<use xlink:href="#c" y="10"/>
+<use xlink:href="#c" y="20"/>
+<use xlink:href="#c" x="10" y="20"/>
+<g id="d">
+<rect height="10" width="10" x="4" y="4" fill="#C00"/>
+<path d="m4 14h10v-10l-1 1v 8h-8z" fill="#a40000"/>
+<path d="m4 14v-10h10l-1 1h-8v8z" fill="#ef2929"/>
+</g>
+<use xlink:href="#d" x="10"/>
+<use xlink:href="#d" x="10" y="10"/>
+<use xlink:href="#d" x="20" y="10"/>
+</svg>
diff --git a/src/pentobi/pentobi.conf.in b/src/pentobi/pentobi.conf.in
new file mode 100644 (file)
index 0000000..e128c3b
--- /dev/null
@@ -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 (file)
index 0000000..a947864
Binary files /dev/null and b/src/pentobi/pentobi.ico differ
diff --git a/src/pentobi/pentobi.rc b/src/pentobi/pentobi.rc
new file mode 100644 (file)
index 0000000..3c6d0d5
--- /dev/null
@@ -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 (file)
index 0000000..0a8988a
--- /dev/null
@@ -0,0 +1,37 @@
+<!DOCTYPE RCC>
+<RCC version="1.0">
+<qresource prefix="/pentobi">
+<file>icons/pentobi.png</file>
+<file>icons/pentobi-16.png</file>
+<file>icons/pentobi-32.png</file>
+<file>icons/pentobi-backward.png</file>
+<file>icons/pentobi-backward-16.png</file>
+<file>icons/pentobi-beginning.png</file>
+<file>icons/pentobi-beginning-16.png</file>
+<file>icons/pentobi-computer-colors.png</file>
+<file>icons/pentobi-computer-colors-16.png</file>
+<file>icons/pentobi-end.png</file>
+<file>icons/pentobi-end-16.png</file>
+<file>icons/pentobi-flip-horizontal.png</file>
+<file>icons/pentobi-flip-vertical.png</file>
+<file>icons/pentobi-forward.png</file>
+<file>icons/pentobi-forward-16.png</file>
+<file>icons/pentobi-newgame.png</file>
+<file>icons/pentobi-newgame-16.png</file>
+<file>icons/pentobi-next-piece.png</file>
+<file>icons/pentobi-next-variation.png</file>
+<file>icons/pentobi-next-variation-16.png</file>
+<file>icons/pentobi-piece-clear.png</file>
+<file>icons/pentobi-play.png</file>
+<file>icons/pentobi-play-16.png</file>
+<file>icons/pentobi-previous-piece.png</file>
+<file>icons/pentobi-previous-variation.png</file>
+<file>icons/pentobi-previous-variation-16.png</file>
+<file>icons/pentobi-rated-game.png</file>
+<file>icons/pentobi-rated-game-16.png</file>
+<file>icons/pentobi-rotate-left.png</file>
+<file>icons/pentobi-rotate-right.png</file>
+<file>icons/pentobi-undo.png</file>
+<file>icons/pentobi-undo-16.png</file>
+</qresource>
+</RCC>
diff --git a/src/pentobi/resources_2x.qrc b/src/pentobi/resources_2x.qrc
new file mode 100644 (file)
index 0000000..0af20b7
--- /dev/null
@@ -0,0 +1,37 @@
+<!DOCTYPE RCC>
+<RCC version="1.0">
+<qresource prefix="/pentobi">
+<file>icons/pentobi@2x.png</file>
+<file>icons/pentobi-16@2x.png</file>
+<file>icons/pentobi-32@2x.png</file>
+<file>icons/pentobi-backward@2x.png</file>
+<file>icons/pentobi-backward-16@2x.png</file>
+<file>icons/pentobi-beginning@2x.png</file>
+<file>icons/pentobi-beginning-16@2x.png</file>
+<file>icons/pentobi-computer-colors@2x.png</file>
+<file>icons/pentobi-computer-colors-16@2x.png</file>
+<file>icons/pentobi-end@2x.png</file>
+<file>icons/pentobi-end-16@2x.png</file>
+<file>icons/pentobi-flip-horizontal@2x.png</file>
+<file>icons/pentobi-flip-vertical@2x.png</file>
+<file>icons/pentobi-forward@2x.png</file>
+<file>icons/pentobi-forward-16@2x.png</file>
+<file>icons/pentobi-newgame@2x.png</file>
+<file>icons/pentobi-newgame-16@2x.png</file>
+<file>icons/pentobi-next-piece@2x.png</file>
+<file>icons/pentobi-next-variation@2x.png</file>
+<file>icons/pentobi-next-variation-16@2x.png</file>
+<file>icons/pentobi-piece-clear@2x.png</file>
+<file>icons/pentobi-play@2x.png</file>
+<file>icons/pentobi-play-16@2x.png</file>
+<file>icons/pentobi-previous-piece@2x.png</file>
+<file>icons/pentobi-previous-variation@2x.png</file>
+<file>icons/pentobi-previous-variation-16@2x.png</file>
+<file>icons/pentobi-rated-game@2x.png</file>
+<file>icons/pentobi-rated-game-16@2x.png</file>
+<file>icons/pentobi-rotate-left@2x.png</file>
+<file>icons/pentobi-rotate-right@2x.png</file>
+<file>icons/pentobi-undo@2x.png</file>
+<file>icons/pentobi-undo-16@2x.png</file>
+</qresource>
+</RCC>
diff --git a/src/pentobi/translations/pentobi.ts b/src/pentobi/translations/pentobi.ts
new file mode 100644 (file)
index 0000000..60127d0
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+    <name>MainWindow</name>
+    <message numerus="yes">
+        <source>%n move(s)</source>
+        <translation>
+            <numerusform>%n move</numerusform>
+            <numerusform>%n moves</numerusform>
+        </translation>
+    </message>
+</context>
+</TS>
diff --git a/src/pentobi/translations/pentobi_de.ts b/src/pentobi/translations/pentobi_de.ts
new file mode 100644 (file)
index 0000000..029a4c8
--- /dev/null
@@ -0,0 +1,1116 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="de_DE">
+<context>
+    <name>AnalyzeGameWidget</name>
+    <message>
+        <source>Win</source>
+        <translation>Gewinn</translation>
+    </message>
+    <message>
+        <source>Loss</source>
+        <translation>Verlust</translation>
+    </message>
+    <message>
+        <source>Running game analysis...</source>
+        <translation>Spiel wird analysiert ...</translation>
+    </message>
+</context>
+<context>
+    <name>AnalyzeGameWindow</name>
+    <message>
+        <source>Game Analysis</source>
+        <translation>Spielanalyse</translation>
+    </message>
+</context>
+<context>
+    <name>AnalyzeSpeedDialog</name>
+    <message>
+        <source>Fast</source>
+        <translation>Schnell</translation>
+    </message>
+    <message>
+        <source>Normal</source>
+        <translation>Normal</translation>
+    </message>
+    <message>
+        <source>Slow</source>
+        <translation>Langsam</translation>
+    </message>
+    <message>
+        <source>Analysis speed:</source>
+        <translation>Analysegeschwindigkeit:</translation>
+    </message>
+</context>
+<context>
+    <name>MainWindow</name>
+    <message>
+        <source>About Pentobi</source>
+        <translation>Über Pentobi</translation>
+    </message>
+    <message>
+        <source>Next &amp;Color</source>
+        <translation>Nächste &amp;Farbe</translation>
+    </message>
+    <message>
+        <source>S&amp;etup Mode</source>
+        <translation>&amp;Stellungsaufbau</translation>
+    </message>
+    <message>
+        <source>Could not read file &apos;%1&apos;</source>
+        <translation>Datei &apos;%1&apos; konnte nicht gelesen werden</translation>
+    </message>
+    <message>
+        <source>The file is not a valid Blokus SGF file.</source>
+        <translation>Die Datei ist keine gültige Blokus-SGF-Datei.</translation>
+    </message>
+    <message>
+        <source>Truncate this subtree?</source>
+        <translation>Diesen Teilbaum abschneiden?</translation>
+    </message>
+    <message>
+        <source>This position and all following moves and variations will be removed from the game tree.</source>
+        <translation>Diese Brettstellung und alle folgenden Züge und Varianten werden aus dem Spielbaum entfernt.</translation>
+    </message>
+    <message>
+        <source>Truncate</source>
+        <translation>Abschneiden</translation>
+    </message>
+    <message>
+        <source>Truncate children?</source>
+        <translation>Kindknoten abschneiden?</translation>
+    </message>
+    <message>
+        <source>All following moves and variations will be removed from the game tree.</source>
+        <translation>Alle folgenden Züge und Varianten werden aus dem Spielbaum entfernt.</translation>
+    </message>
+    <message>
+        <source>Truncate Children</source>
+        <translation>Kindknoten abschneiden</translation>
+    </message>
+    <message>
+        <source>Could not delete %1</source>
+        <translation>%1 konnte nicht gelöscht werden</translation>
+    </message>
+    <message>
+        <source>Rated game</source>
+        <translation>Gewertetes Spiel</translation>
+    </message>
+    <message>
+        <source>Continuing unfinished rated game.</source>
+        <translation>Unbeendetes gewertetes Spiel wird fortgesetzt.</translation>
+    </message>
+    <message>
+        <source>You play %1 in this game.</source>
+        <translation>Sie spielen %1 in diesem Spiel.</translation>
+    </message>
+    <message>
+        <source>&amp;copy; 2011&amp;ndash;%1 Markus Enzenberger</source>
+        <translation>&amp;copy; 2011&amp;ndash;%1 Markus Enzenberger</translation>
+    </message>
+    <message>
+        <source>Analyze Game</source>
+        <translation>Spiel analysieren</translation>
+    </message>
+    <message>
+        <source>&amp;About Pentobi</source>
+        <translation>Über &amp;Pentobi</translation>
+    </message>
+    <message>
+        <source>&amp;Analyze Game...</source>
+        <translation>Spiel &amp;analysieren ...</translation>
+    </message>
+    <message>
+        <source>B&amp;ackward</source>
+        <translation>&amp;Zurück</translation>
+    </message>
+    <message>
+        <source>Go one move backward</source>
+        <translation>Einen Zug zurück gehen</translation>
+    </message>
+    <message>
+        <source>Back to &amp;Main Variation</source>
+        <translation>Zurück zu Hau&amp;ptvariante</translation>
+    </message>
+    <message>
+        <source>&amp;Bad</source>
+        <translation>Schl&amp;echt</translation>
+    </message>
+    <message>
+        <source>&amp;Beginning</source>
+        <translation>&amp;Anfang</translation>
+    </message>
+    <message>
+        <source>Go to beginning of game</source>
+        <translation>Zum Anfang des Spiels gehen</translation>
+    </message>
+    <message>
+        <source>Beginning of Bran&amp;ch</source>
+        <translation>Anfang der Verz&amp;weigung</translation>
+    </message>
+    <message>
+        <source>Clear Piece</source>
+        <translation>Spielstein löschen</translation>
+    </message>
+    <message>
+        <source>&amp;Computer Colors</source>
+        <translation>&amp;Computer-Farben</translation>
+    </message>
+    <message>
+        <source>Set the colors played by the computer</source>
+        <translation>Die vom Computer gespielten Farben festlegen</translation>
+    </message>
+    <message>
+        <source>C&amp;oordinates</source>
+        <translation>K&amp;oordinaten</translation>
+    </message>
+    <message>
+        <source>&amp;Delete All Variations</source>
+        <translation>Alle &amp;Varianten löschen</translation>
+    </message>
+    <message>
+        <source>&amp;Doubtful</source>
+        <translation>&amp;Zweifelhaft</translation>
+    </message>
+    <message>
+        <source>&amp;End</source>
+        <translation>&amp;Ende</translation>
+    </message>
+    <message>
+        <source>Go to end of moves</source>
+        <translation>Zum Ende der Züge gehen</translation>
+    </message>
+    <message>
+        <source>&amp;ASCII Art</source>
+        <translation>&amp;ASCII-Art</translation>
+    </message>
+    <message>
+        <source>I&amp;mage</source>
+        <translation>&amp;Grafik</translation>
+    </message>
+    <message>
+        <source>&amp;Find Move</source>
+        <translation>Zug fin&amp;den</translation>
+    </message>
+    <message>
+        <source>Flip Horizontally</source>
+        <translation>Waagrecht umdrehen</translation>
+    </message>
+    <message>
+        <source>Flip Vertically</source>
+        <translation>Senkrecht umdrehen</translation>
+    </message>
+    <message>
+        <source>&amp;Forward</source>
+        <translation>&amp;Vorwärts</translation>
+    </message>
+    <message>
+        <source>Go one move forward</source>
+        <translation>Einen Zug vorwärts gehen</translation>
+    </message>
+    <message>
+        <source>&amp;Fullscreen</source>
+        <translation>Voll&amp;bild</translation>
+    </message>
+    <message>
+        <source>Ga&amp;me Info</source>
+        <translation>Spielinf&amp;ormation</translation>
+    </message>
+    <message>
+        <source>St&amp;op</source>
+        <translation>St&amp;opp</translation>
+    </message>
+    <message>
+        <source>&amp;Duo</source>
+        <translation>&amp;Duo</translation>
+    </message>
+    <message>
+        <source>&amp;Good</source>
+        <translation>&amp;Gut</translation>
+    </message>
+    <message>
+        <source>Game analysis is only possible in the main variation.</source>
+        <translation>Spielanalyse ist nur in der Hauptvariante möglich.</translation>
+    </message>
+    <message>
+        <source>Find Next &amp;Comment</source>
+        <translation>Nächsten &amp;Kommentar finden</translation>
+    </message>
+    <message>
+        <source>&amp;Go to Move...</source>
+        <translation>&amp;Gehe zu Zug ...</translation>
+    </message>
+    <message>
+        <source>Pentobi &amp;Help</source>
+        <translation>Pentobi-&amp;Hilfe</translation>
+    </message>
+    <message>
+        <source>I&amp;nteresting</source>
+        <translation>I&amp;nteressant</translation>
+    </message>
+    <message>
+        <source>&amp;Keep Only Position</source>
+        <translation>Nur &amp;Brettstellung behalten</translation>
+    </message>
+    <message>
+        <source>Keep Only &amp;Subtree</source>
+        <translation>Nur &amp;Teilbaum behalten</translation>
+    </message>
+    <message>
+        <source>Leave Fullscreen</source>
+        <translation>Vollbild verlassen</translation>
+    </message>
+    <message>
+        <source>M&amp;ake Main Variation</source>
+        <translation>Zu &amp;Hauptvariante machen</translation>
+    </message>
+    <message>
+        <source>Move Variation D&amp;own</source>
+        <translation>Variante nach &amp;unten schieben</translation>
+    </message>
+    <message>
+        <source>Move Variation &amp;Up</source>
+        <translation>Variante nach &amp;oben schieben</translation>
+    </message>
+    <message>
+        <source>&amp;1</source>
+        <translation>&amp;1</translation>
+    </message>
+    <message>
+        <source>&amp;2</source>
+        <translation>&amp;2</translation>
+    </message>
+    <message>
+        <source>&amp;3</source>
+        <translation>&amp;3</translation>
+    </message>
+    <message>
+        <source>&amp;4</source>
+        <translation>&amp;4</translation>
+    </message>
+    <message>
+        <source>&amp;5</source>
+        <translation>&amp;5</translation>
+    </message>
+    <message>
+        <source>&amp;6</source>
+        <translation>&amp;6</translation>
+    </message>
+    <message>
+        <source>&amp;7</source>
+        <translation>&amp;7</translation>
+    </message>
+    <message>
+        <source>&amp;8</source>
+        <translation>&amp;8</translation>
+    </message>
+    <message>
+        <source>&amp;None</source>
+        <comment>move numbers</comment>
+        <translation>&amp;Keine</translation>
+    </message>
+    <message>
+        <source>Next Piece</source>
+        <translation>Nächster Spielstein</translation>
+    </message>
+    <message>
+        <source>&amp;Next Variation</source>
+        <translation>&amp;Nächste Variante</translation>
+    </message>
+    <message>
+        <source>Go to next variation</source>
+        <translation>Zur nächsten Variante gehen</translation>
+    </message>
+    <message>
+        <source>&amp;Rated Game</source>
+        <translation>Ge&amp;wertetes Spiel</translation>
+    </message>
+    <message>
+        <source>&amp;New</source>
+        <translation>&amp;Neu</translation>
+    </message>
+    <message>
+        <source>Start a new game</source>
+        <translation>Ein neues Spiel beginnen</translation>
+    </message>
+    <message>
+        <source>N&amp;one</source>
+        <comment>move annotation</comment>
+        <translation>&amp;Keine</translation>
+    </message>
+    <message>
+        <source>&amp;Open...</source>
+        <translation>Öffn&amp;en ...</translation>
+    </message>
+    <message>
+        <source>&amp;Play</source>
+        <translation>&amp;Spielen</translation>
+    </message>
+    <message>
+        <source>Play &amp;Single Move</source>
+        <translation>&amp;Einzelnen Zug spielen</translation>
+    </message>
+    <message>
+        <source>Previous Piece</source>
+        <translation>Vorheriger Spielstein</translation>
+    </message>
+    <message>
+        <source>&amp;Previous Variation</source>
+        <translation>Vor&amp;herige Variante</translation>
+    </message>
+    <message>
+        <source>Go to previous variation</source>
+        <translation>Zur vorherigen Variante gehen</translation>
+    </message>
+    <message>
+        <source>Rotate Anticlockwise</source>
+        <translation>Gegen den Uhrzeigersinn drehen</translation>
+    </message>
+    <message>
+        <source>Rotate Clockwise</source>
+        <translation>Im Uhrzeigersinn drehen</translation>
+    </message>
+    <message>
+        <source>&amp;Quit</source>
+        <translation>&amp;Beenden</translation>
+    </message>
+    <message>
+        <source>&amp;Save</source>
+        <translation>&amp;Speichern</translation>
+    </message>
+    <message>
+        <source>Save &amp;As...</source>
+        <translation>Speichern &amp;unter ...</translation>
+    </message>
+    <message>
+        <source>&amp;Comment</source>
+        <translation>&amp;Kommentar</translation>
+    </message>
+    <message>
+        <source>&amp;Rating</source>
+        <translation>&amp;Wertung</translation>
+    </message>
+    <message>
+        <source>&amp;No Text</source>
+        <translation>&amp;Kein Text</translation>
+    </message>
+    <message>
+        <source>Text &amp;Beside Icons</source>
+        <translation>Text n&amp;eben Symbolen</translation>
+    </message>
+    <message>
+        <source>Text Bel&amp;ow Icons</source>
+        <translation>Text &amp;unter Symbolen</translation>
+    </message>
+    <message>
+        <source>&amp;Text Only</source>
+        <translation>&amp;Nur Text</translation>
+    </message>
+    <message>
+        <source>&amp;System Default</source>
+        <translation>&amp;Systemvorgabe</translation>
+    </message>
+    <message>
+        <source>&amp;Truncate</source>
+        <translation>&amp;Abschneiden</translation>
+    </message>
+    <message>
+        <source>Truncate C&amp;hildren</source>
+        <translation>&amp;Kindknoten abschneiden</translation>
+    </message>
+    <message>
+        <source>Show &amp;Variations</source>
+        <translation>&amp;Varianten zeigen</translation>
+    </message>
+    <message>
+        <source>&amp;Undo Move</source>
+        <translation>Zug rück&amp;gängig</translation>
+    </message>
+    <message>
+        <source>V&amp;ery Bad</source>
+        <translation>Seh&amp;r schlecht</translation>
+    </message>
+    <message>
+        <source>&amp;Very Good</source>
+        <translation>&amp;Sehr gut</translation>
+    </message>
+    <message>
+        <source>&amp;Edit</source>
+        <translation>&amp;Bearbeiten</translation>
+    </message>
+    <message>
+        <source>&amp;Move Annotation</source>
+        <translation>&amp;Zugkommentierung</translation>
+    </message>
+    <message>
+        <source>&amp;View</source>
+        <translation>&amp;Ansicht</translation>
+    </message>
+    <message>
+        <source>Toolbar T&amp;ext</source>
+        <translation>Werkzeugleistent&amp;ext</translation>
+    </message>
+    <message>
+        <source>&amp;Computer</source>
+        <translation>&amp;Computer</translation>
+    </message>
+    <message>
+        <source>&amp;Tools</source>
+        <translation>E&amp;xtras</translation>
+    </message>
+    <message>
+        <source>&amp;Help</source>
+        <translation>&amp;Hilfe</translation>
+    </message>
+    <message>
+        <source>Delete all variations?</source>
+        <translation>Alle Varianten löschen?</translation>
+    </message>
+    <message>
+        <source>All variations but the main variation will be removed from the game tree.</source>
+        <translation>Alle Varianten außer der Hauptvariante werden aus dem Spielbaum entfernt.</translation>
+    </message>
+    <message>
+        <source>Delete Variations</source>
+        <translation>Varianten löschen</translation>
+    </message>
+    <message>
+        <source>&amp;Toolbar</source>
+        <translation>&amp;Werkzeugleiste</translation>
+    </message>
+    <message>
+        <source>Text files (*.txt);;All files (*)</source>
+        <translation>Textdateien (*.txt);;Alle Dateien (*)</translation>
+    </message>
+    <message>
+        <source>No comment found</source>
+        <translation>Kein Kommentar gefunden</translation>
+    </message>
+    <message>
+        <source>Blokus games (*.blksgf);;All files (*)</source>
+        <translation>Blokus-Partien (*.blksgf);;Alle Dateien (*)</translation>
+    </message>
+    <message>
+        <source>Move number:</source>
+        <translation>Zugnummer:</translation>
+    </message>
+    <message>
+        <source>Go to Move</source>
+        <translation>Gehe zu Zug</translation>
+    </message>
+    <message>
+        <source>Keep only position?</source>
+        <translation>Nur Brettstellung behalten?</translation>
+    </message>
+    <message>
+        <source>All previous and following moves and variations will be removed from the game tree.</source>
+        <translation>Alle vorhergehenden und nachfolgenden Züge und Varianten werden aus dem Spielbaum entfernt.</translation>
+    </message>
+    <message>
+        <source>Keep only subtree?</source>
+        <translation>Nur Teilbaum behalten?</translation>
+    </message>
+    <message>
+        <source>All previous moves and variations will be removed from the game tree.</source>
+        <translation>Alle vorhergehenden Züge und Varianten werden aus dem Spielbaum entfernt.</translation>
+    </message>
+    <message>
+        <source>Start rated game?</source>
+        <translation>Gewertetes Spiel beginnen?</translation>
+    </message>
+    <message>
+        <source>In this game, you play %1 against Pentobi level&amp;nbsp;%2.</source>
+        <translation>In diesem Spiel spielen Sie %1 gegen Pentobi Spielstufe&amp;nbsp;%2.</translation>
+    </message>
+    <message>
+        <source>&amp;Start Game</source>
+        <translation>&amp;Spiel beginnen</translation>
+    </message>
+    <message>
+        <source>Pentobi %1 (level %2)</source>
+        <extracomment>The first argument is the version of Pentobi</extracomment>
+        <translation>Pentobi %1 (Stufe %2)</translation>
+    </message>
+    <message>
+        <source>Human</source>
+        <translation>Mensch</translation>
+    </message>
+    <message>
+        <source>Open</source>
+        <translation>Öffnen</translation>
+    </message>
+    <message>
+        <source>The file could not be saved.</source>
+        <translation>Die Datei konnte nicht gespeichert werden.</translation>
+    </message>
+    <message>
+        <source>%1: %2</source>
+        <extracomment>Error message if file cannot be saved. %1 is replaced by the file name, %2 by the error message of the operating system.</extracomment>
+        <translation>%1: %2</translation>
+    </message>
+    <message>
+        <source>Untitled Game.blksgf</source>
+        <translation>Unbenanntes Spiel.blksgf</translation>
+    </message>
+    <message>
+        <source>Untitled Game %1.blksgf</source>
+        <translation>Unbenanntes Spiel %1.blksgf</translation>
+    </message>
+    <message>
+        <source>Save</source>
+        <translation>Speichern</translation>
+    </message>
+    <message>
+        <source>Pentobi</source>
+        <translation>Pentobi</translation>
+    </message>
+    <message>
+        <source>Version %1</source>
+        <translation>Version %1</translation>
+    </message>
+    <message>
+        <source>Computer opponent for the board game Blokus.</source>
+        <translation>Computer-Gegner für das Brettspiel Blokus.</translation>
+    </message>
+    <message>
+        <source>The file has been modified.</source>
+        <translation>Die Datei wurde geändert.</translation>
+    </message>
+    <message>
+        <source>Do you want to save your changes?</source>
+        <translation>Änderungen speichern?</translation>
+    </message>
+    <message>
+        <source>&amp;Don&apos;t Save</source>
+        <translation>&amp;Nicht speichern</translation>
+    </message>
+    <message>
+        <source>The current game is not finished.</source>
+        <translation>Das gegenwärtige Spiel ist nicht beendet.</translation>
+    </message>
+    <message>
+        <source>Do you want to abort the game?</source>
+        <translation>Möchten Sie das Spiel verwerfen?</translation>
+    </message>
+    <message>
+        <source>&amp;Abort Game</source>
+        <translation>Spiel &amp;verwerfen</translation>
+    </message>
+    <message>
+        <source>&amp;9</source>
+        <translation>&amp;9</translation>
+    </message>
+    <message>
+        <source>&amp;All with Number</source>
+        <translation>&amp;Alle mit Nummer</translation>
+    </message>
+    <message>
+        <source>Last with &amp;Dot</source>
+        <translation>Letzter mit &amp;Punkt</translation>
+    </message>
+    <message>
+        <source>&amp;Last with Number</source>
+        <translation>Letzter mit &amp;Nummer</translation>
+    </message>
+    <message>
+        <source>Start a rated game</source>
+        <translation>Ein gewertetes Spiel beginnen</translation>
+    </message>
+    <message>
+        <source>Classic (&amp;3 Players)</source>
+        <translation>Klassisch (&amp;3 Spieler)</translation>
+    </message>
+    <message>
+        <source>&amp;Game</source>
+        <translation>&amp;Spiel</translation>
+    </message>
+    <message>
+        <source>Game &amp;Variant</source>
+        <translation>Spiel&amp;variante</translation>
+    </message>
+    <message>
+        <source>Open R&amp;ecent</source>
+        <translation>&amp;Zuletzt benutzte Dateien</translation>
+    </message>
+    <message>
+        <source>E&amp;xport</source>
+        <translation>E&amp;xportieren</translation>
+    </message>
+    <message>
+        <source>G&amp;o</source>
+        <translation>&amp;Gehe zu</translation>
+    </message>
+    <message>
+        <source>&amp;Move Marking</source>
+        <translation>&amp;Zugmarkierung</translation>
+    </message>
+    <message>
+        <source>Export Image</source>
+        <translation>Grafik exportieren</translation>
+    </message>
+    <message>
+        <source>Image size:</source>
+        <translation>Bildgröße:</translation>
+    </message>
+    <message>
+        <source>The end of the tree was reached.</source>
+        <translation>Das Ende des Spielbaums wurde erreicht.</translation>
+    </message>
+    <message>
+        <source>Continue the search from the start of the tree?</source>
+        <translation>Die Suche vom Beginn des Spielbaums fortsetzen?</translation>
+    </message>
+    <message>
+        <source>Continue From Start</source>
+        <translation>Suche vom Beginn fortsetzen</translation>
+    </message>
+    <message>
+        <source>Show &amp;Rating</source>
+        <translation>&amp;Wertung zeigen</translation>
+    </message>
+    <message>
+        <source>Pentobi Help</source>
+        <translation>Pentobi-Hilfe</translation>
+    </message>
+    <message>
+        <source>Keep Only Position</source>
+        <translation>Nur Brettstellung behalten</translation>
+    </message>
+    <message>
+        <source>Keep Only Subtree</source>
+        <translation>Nur Teilbaum behalten</translation>
+    </message>
+    <message>
+        <source>[*]%1</source>
+        <translation>[*]%1</translation>
+    </message>
+    <message>
+        <source>Setup mode cannot be used if moves have been played.</source>
+        <translation>Stellungsaufbau-Modus kann nicht benutzt werden, wenn Züge gespielt wurden.</translation>
+    </message>
+    <message>
+        <source>Setup mode</source>
+        <translation>Stellungsaufbau</translation>
+    </message>
+    <message>
+        <source>The game ends in a tie.</source>
+        <translation>Das Spiel endet in einem Unentschieden.</translation>
+    </message>
+    <message>
+        <source>The game ends in a tie between all colors.</source>
+        <translation>Das Spiel endet in einem Unentschieden zwischen allen Farben.</translation>
+    </message>
+    <message>
+        <source>The game ends in a tie between Blue, Yellow and Red.</source>
+        <translation>Das Spiel endet in einem Unentschieden zwischen Blau, Gelb und Rot.</translation>
+    </message>
+    <message>
+        <source>The game ends in a tie between Blue, Yellow and Green.</source>
+        <translation>Das Spiel endet in einem Unentschieden zwischen Blau, Gelb und Grün.</translation>
+    </message>
+    <message>
+        <source>The game ends in a tie between Blue, Red and Green.</source>
+        <translation>Das Spiel endet in einem Unentschieden zwischen Blau, Rot und Grün.</translation>
+    </message>
+    <message>
+        <source>The game ends in a tie between Yellow, Red and Green.</source>
+        <translation>Das Spiel endet in einem Unentschieden zwischen Gelb, Rot und Grün.</translation>
+    </message>
+    <message>
+        <source>The game ends in a tie between Blue and Yellow.</source>
+        <translation>Das Spiel endet in einem Unentschieden zwischen Blau und Gelb.</translation>
+    </message>
+    <message>
+        <source>The game ends in a tie between Blue and Red.</source>
+        <translation>Das Spiel endet in einem Unentschieden zwischen Blau und Rot.</translation>
+    </message>
+    <message>
+        <source>The game ends in a tie between Blue and Green.</source>
+        <translation>Das Spiel endet in einem Unentschieden zwischen Blau und Grün.</translation>
+    </message>
+    <message>
+        <source>The game ends in a tie between Yellow and Red.</source>
+        <translation>Das Spiel endet in einem Unentschieden zwischen Gelb und Rot.</translation>
+    </message>
+    <message>
+        <source>The game ends in a tie between Yellow and Green.</source>
+        <translation>Das Spiel endet in einem Unentschieden zwischen Gelb und Grün.</translation>
+    </message>
+    <message>
+        <source>The game ends in a tie between Red and Green.</source>
+        <translation>Das Spiel endet in einem Unentschieden zwischen Rot und Grün.</translation>
+    </message>
+    <message>
+        <source>Blue wins.</source>
+        <translation>Blau gewinnt.</translation>
+    </message>
+    <message>
+        <source>Yellow wins.</source>
+        <translation>Gelb gewinnt.</translation>
+    </message>
+    <message>
+        <source>Red wins.</source>
+        <translation>Rot gewinnt.</translation>
+    </message>
+    <message>
+        <source>Green wins.</source>
+        <translation>Grün gewinnt.</translation>
+    </message>
+    <message>
+        <source>Your rating has increased from %1 to %2.</source>
+        <translation>Ihre Wertung hat sich von %1 auf %2 erhöht.</translation>
+    </message>
+    <message>
+        <source>Your rating stays at %1.</source>
+        <translation>Ihre Wertung bleibt auf %1.</translation>
+    </message>
+    <message>
+        <source>Your rating has decreased from %1 to %2.</source>
+        <translation>Ihre Wertung hat sich von %1 auf %2 erniedrigt.</translation>
+    </message>
+    <message>
+        <source>Error in file &apos;%1&apos;</source>
+        <translation>Fehler in Datei &apos;%1&apos;</translation>
+    </message>
+    <message>
+        <source>Move %1</source>
+        <translation>Zug %1</translation>
+    </message>
+    <message numerus="yes">
+        <source>%n move(s)</source>
+        <translation>
+            <numerusform>%n Zug</numerusform>
+            <numerusform>%n Züge</numerusform>
+        </translation>
+    </message>
+    <message>
+        <source>Move %1 of %2</source>
+        <translation>Zug %1 von %2</translation>
+    </message>
+    <message>
+        <source>Move %1 of %2 in variation %3</source>
+        <translation>Zug %1 von %2 in Variante %3</translation>
+    </message>
+    <message>
+        <source>Make the computer continue to play the current color</source>
+        <translation>Den Computer die gegenwärtige Farbe weiterspielen lassen</translation>
+    </message>
+    <message>
+        <source>Make the computer play the current color</source>
+        <translation>Den Computer die gegenwärtige Farbe spielen lassen</translation>
+    </message>
+    <message>
+        <source>Classic (&amp;4 Players)</source>
+        <translation>Klassisch (&amp;4 Spieler)</translation>
+    </message>
+    <message>
+        <source>J&amp;unior</source>
+        <translation>J&amp;unior</translation>
+    </message>
+    <message>
+        <source>Classic (&amp;2 Players)</source>
+        <translation>Klassisch (&amp;2 Spieler)</translation>
+    </message>
+    <message>
+        <source>Nexos (&amp;2 Players)</source>
+        <translation>Nexos (&amp;2 Spieler)</translation>
+    </message>
+    <message>
+        <source>Nexos (&amp;4 Players)</source>
+        <translation>Nexos (&amp;4 Spieler)</translation>
+    </message>
+    <message>
+        <source>Trigon (&amp;2 Players)</source>
+        <translation>Trigon (&amp;2 Spieler)</translation>
+    </message>
+    <message>
+        <source>Trigon (&amp;3 Players)</source>
+        <translation>Trigon (&amp;3 Spieler)</translation>
+    </message>
+    <message>
+        <source>Trigon (&amp;4 Players)</source>
+        <translation>Trigon (&amp;4 Spieler)</translation>
+    </message>
+    <message>
+        <source>&amp;Classic</source>
+        <translation>&amp;Klassisch</translation>
+    </message>
+    <message>
+        <source>&amp;Trigon</source>
+        <translation>&amp;Trigon</translation>
+    </message>
+    <message>
+        <source>&amp;Nexos</source>
+        <translation>&amp;Nexos</translation>
+    </message>
+    <message>
+        <source>Blue wins with 1 point.</source>
+        <translation>Blau gewinnt mit 1 Punkt.</translation>
+    </message>
+    <message>
+        <source>Blue wins with %1 points.</source>
+        <translation>Blau gewinnt mit %1 Punkten.</translation>
+    </message>
+    <message>
+        <source>Green wins with 1 point.</source>
+        <translation>Grün gewinnt mit 1 Punkt.</translation>
+    </message>
+    <message>
+        <source>Green wins with %1 points.</source>
+        <translation>Grün gewinnt mit %1 Punkten.</translation>
+    </message>
+    <message>
+        <source>Blue/Red wins with 1 point.</source>
+        <translation>Blau/Rot gewinnt mit 1 Punkt.</translation>
+    </message>
+    <message>
+        <source>Blue/Red wins with %1 points.</source>
+        <translation>Blau/Rot gewinnt mit %1 Punkten.</translation>
+    </message>
+    <message>
+        <source>Yellow/Green wins with 1 point.</source>
+        <translation>Gelb/Grün gewinnt mit 1 Punkt.</translation>
+    </message>
+    <message>
+        <source>Yellow/Green wins with %1 points.</source>
+        <translation>Gelb/Grün gewinnt mit %1 Punkten.</translation>
+    </message>
+    <message>
+        <source>Callisto (&amp;2 Players)</source>
+        <translation>Callisto (&amp;2 Spieler)</translation>
+    </message>
+    <message>
+        <source>Callisto (&amp;3 Players)</source>
+        <translation>Callisto (&amp;3 Spieler)</translation>
+    </message>
+    <message>
+        <source>Callisto (&amp;4 Players)</source>
+        <translation>Callisto (&amp;4 Spieler)</translation>
+    </message>
+    <message>
+        <source>C&amp;allisto</source>
+        <translation>&amp;Callisto</translation>
+    </message>
+    <message>
+        <source>Green wins (tie resolved).</source>
+        <translation>Grün gewinnt (Unentschieden aufgelöst).</translation>
+    </message>
+    <message>
+        <source>Yellow/Green wins (tie resolved).</source>
+        <translation>Gelb/Grün gewinnt (Unentschieden aufgelöst).</translation>
+    </message>
+    <message>
+        <source>Red wins (tie resolved).</source>
+        <translation>Rot gewinnt (Unentschieden aufgelöst).</translation>
+    </message>
+    <message>
+        <source>Yellow wins (tie resolved).</source>
+        <translation>Gelb gewinnt (Unentschieden aufgelöst).</translation>
+    </message>
+    <message>
+        <source>&amp;Level (Classic, 4 Players)</source>
+        <translation>Spielst&amp;ufe (Klassisch, 4 Spieler)</translation>
+    </message>
+    <message>
+        <source>&amp;Level (Classic, 2 Players)</source>
+        <translation>Spielst&amp;ufe (Klassisch, 2 Spieler)</translation>
+    </message>
+    <message>
+        <source>&amp;Level (Classic, 3 Players)</source>
+        <translation>Spielst&amp;ufe (Klassisch, 3 Spieler)</translation>
+    </message>
+    <message>
+        <source>&amp;Level (Duo)</source>
+        <translation>Spielst&amp;ufe (Duo)</translation>
+    </message>
+    <message>
+        <source>&amp;Level (Trigon, 4 Players)</source>
+        <translation>Spielst&amp;ufe (Trigon, 4 Spieler)</translation>
+    </message>
+    <message>
+        <source>&amp;Level (Trigon, 2 Players)</source>
+        <translation>Spielst&amp;ufe (Trigon, 2 Spieler)</translation>
+    </message>
+    <message>
+        <source>&amp;Level (Trigon, 3 Players)</source>
+        <translation>Spielst&amp;ufe (Trigon, 3 Spieler)</translation>
+    </message>
+    <message>
+        <source>&amp;Level (Junior)</source>
+        <translation>Spielst&amp;ufe (Junior)</translation>
+    </message>
+    <message>
+        <source>&amp;Level (Nexos, 4 Players)</source>
+        <translation>Spielst&amp;ufe (Nexos, 4 Spieler)</translation>
+    </message>
+    <message>
+        <source>&amp;Level (Nexos, 2 Players)</source>
+        <translation>Spielst&amp;ufe (Nexos, 2 Spieler)</translation>
+    </message>
+    <message>
+        <source>&amp;Level (Callisto, 4 Players)</source>
+        <translation>Spielst&amp;ufe (Callisto, 4 Spieler)</translation>
+    </message>
+    <message>
+        <source>&amp;Level (Callisto, 2 Players)</source>
+        <translation>Spielst&amp;ufe (Callisto, 2 Spieler)</translation>
+    </message>
+    <message>
+        <source>&amp;Level (Callisto, 3 Players)</source>
+        <translation>Spielst&amp;ufe (Callisto, 3 Spieler)</translation>
+    </message>
+    <message>
+        <source>Computer is thinking... (up to %1 seconds remaining)</source>
+        <translation>Computer denkt ... (maximal %1 Sekunden verbleibend)</translation>
+    </message>
+    <message>
+        <source>Computer is thinking... (up to %1 minutes remaining)</source>
+        <translation>Computer denkt ... (maximal %1 Minuten verbleibend)</translation>
+    </message>
+    <message>
+        <source>Computer is thinking...</source>
+        <translation>Computer denkt ...</translation>
+    </message>
+</context>
+<context>
+    <name>RatedGamesList</name>
+    <message>
+        <source>Game</source>
+        <translation>Spiel</translation>
+    </message>
+    <message>
+        <source>Your Color</source>
+        <translation>Ihre Farbe</translation>
+    </message>
+    <message>
+        <source>Level</source>
+        <translation>Stufe</translation>
+    </message>
+    <message>
+        <source>Result</source>
+        <translation>Ergebnis</translation>
+    </message>
+    <message>
+        <source>Date</source>
+        <translation>Datum</translation>
+    </message>
+    <message>
+        <source>Win</source>
+        <translation>Gewinn</translation>
+    </message>
+    <message>
+        <source>Tie</source>
+        <translation>Unentschieden</translation>
+    </message>
+    <message>
+        <source>Loss</source>
+        <translation>Verlust</translation>
+    </message>
+</context>
+<context>
+    <name>RatingDialog</name>
+    <message>
+        <source>Rating</source>
+        <translation>Wertung</translation>
+    </message>
+    <message>
+        <source>Your rating:</source>
+        <translation>Ihre Wertung:</translation>
+    </message>
+    <message>
+        <source>Game variant:</source>
+        <translation>Spielvariante:</translation>
+    </message>
+    <message>
+        <source>Number rated games:</source>
+        <translation>Anzahl gewertete Spiele:</translation>
+    </message>
+    <message>
+        <source>Best previous rating:</source>
+        <translation>Beste frühere Wertung:</translation>
+    </message>
+    <message>
+        <source>Recent games:</source>
+        <translation>Zuletzt gespielte Spiele:</translation>
+    </message>
+    <message>
+        <source>&amp;Clear</source>
+        <translation>&amp;Löschen</translation>
+    </message>
+    <message>
+        <source>Clear rating and delete rating history?</source>
+        <translation>Wertung und Wertungsentwicklung löschen?</translation>
+    </message>
+    <message>
+        <source>Clear rating</source>
+        <translation>Wertung löschen</translation>
+    </message>
+    <message>
+        <source>Classic (4 players)</source>
+        <translation>Klassisch (4 Spieler)</translation>
+    </message>
+    <message>
+        <source>Classic (2 players)</source>
+        <translation>Klassisch (2 Spieler)</translation>
+    </message>
+    <message>
+        <source>Classic (3 players)</source>
+        <translation>Klassisch (3 Spieler)</translation>
+    </message>
+    <message>
+        <source>Duo</source>
+        <translation>Duo</translation>
+    </message>
+    <message>
+        <source>Trigon (4 players)</source>
+        <translation>Trigon (4 Spieler)</translation>
+    </message>
+    <message>
+        <source>Trigon (2 players)</source>
+        <translation>Trigon (2 Spieler)</translation>
+    </message>
+    <message>
+        <source>Trigon (3 players)</source>
+        <translation>Trigon (3 Spieler)</translation>
+    </message>
+    <message>
+        <source>Junior</source>
+        <translation>Junior</translation>
+    </message>
+    <message>
+        <source>Recent development:</source>
+        <translation>Aktuelle Entwicklung:</translation>
+    </message>
+    <message>
+        <source>Nexos (4 players)</source>
+        <translation>Nexos (4 Spieler)</translation>
+    </message>
+    <message>
+        <source>Nexos (2 players)</source>
+        <translation>Nexos (2 Spieler)</translation>
+    </message>
+    <message>
+        <source>Callisto (4 players)</source>
+        <translation>Callisto (4 Spieler)</translation>
+    </message>
+    <message>
+        <source>Callisto (2 players)</source>
+        <translation>Callisto (2 Spieler)</translation>
+    </message>
+    <message>
+        <source>Callisto (3 players)</source>
+        <translation>Callisto (3 Spieler)</translation>
+    </message>
+</context>
+<context>
+    <name>main</name>
+    <message>
+        <source>Not enough memory.</source>
+        <translation>Nicht genügend Speicher.</translation>
+    </message>
+    <message>
+        <source>Pentobi</source>
+        <translation>Pentobi</translation>
+    </message>
+</context>
+</TS>
diff --git a/src/pentobi_gtp/CMakeLists.txt b/src/pentobi_gtp/CMakeLists.txt
new file mode 100644 (file)
index 0000000..d42d74c
--- /dev/null
@@ -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 (file)
index 0000000..07734c3
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "Engine.h"
+
+#include <fstream>
+#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<const Search::Node*> 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<bool>(1));
+        else if (name == "auto_param")
+            s.set_auto_param(args.parse<bool>(1));
+        else if (name == "exploration_constant")
+            s.set_exploration_constant(args.parse<Float>(1));
+        else if (name == "expand_threshold")
+            s.set_expand_threshold(args.parse<Float>(1));
+        else if (name == "expand_threshold_inc")
+            s.set_expand_threshold_inc(args.parse<Float>(1));
+        else if (name == "fixed_simulations")
+            p.set_fixed_simulations(args.parse<Float>(1));
+        else if (name == "rave_child_max")
+            s.set_rave_child_max(args.parse<Float>(1));
+        else if (name == "rave_parent_max")
+            s.set_rave_parent_max(args.parse<Float>(1));
+        else if (name == "rave_weight")
+            s.set_rave_weight(args.parse<Float>(1));
+        else if (name == "reuse_subtree")
+            s.set_reuse_subtree(args.parse<bool>(1));
+        else if (name == "use_book")
+            p.set_use_book(args.parse<bool>(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<Player&>(*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 (file)
index 0000000..76418b3
--- /dev/null
@@ -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<PlayerBase> 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 (file)
index 0000000..3fa9087
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include <fstream>
+#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<string> 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<unsigned>("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<RandomGenerator::ResultType>("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<unsigned>("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 (file)
index 0000000..bc3002f
--- /dev/null
@@ -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 (file)
index 0000000..ce003d6
--- /dev/null
@@ -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 <QImage>
+#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 (file)
index 0000000..a28b14d
--- /dev/null
@@ -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 <QObject>
+#include <kio/thumbcreator.h>
+
+//-----------------------------------------------------------------------------
+
+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 (file)
index 0000000..1acd6dc
--- /dev/null
@@ -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 (file)
index 0000000..9bfc6e8
--- /dev/null
@@ -0,0 +1 @@
+Pentobi.pro.user
diff --git a/src/pentobi_qml/CMakeLists.txt b/src/pentobi_qml/CMakeLists.txt
new file mode 100644 (file)
index 0000000..36db59d
--- /dev/null
@@ -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 (file)
index 0000000..28cf24b
--- /dev/null
@@ -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 <cerrno>
+#include <cstring>
+#include <fstream>
+#include <QDebug>
+#include <QSettings>
+#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<PieceModel*>& 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<int>(round(coord.x() - center.x()));
+    int offY = static_cast<int>(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<PieceModel*>& 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<int>(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<Color::IntType>(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<PieceModel> GameModel::pieceModels0()
+{
+    return QQmlListProperty<PieceModel>(this, m_pieceModels0);
+}
+
+QQmlListProperty<PieceModel> GameModel::pieceModels1()
+{
+    return QQmlListProperty<PieceModel>(this, m_pieceModels1);
+}
+
+QQmlListProperty<PieceModel> GameModel::pieceModels2()
+{
+    return QQmlListProperty<PieceModel>(this, m_pieceModels2);
+}
+
+QQmlListProperty<PieceModel> GameModel::pieceModels3()
+{
+    return QQmlListProperty<PieceModel>(this, m_pieceModels3);
+}
+
+void GameModel::playMove(int move)
+{
+    Move mv(static_cast<Move::IntType>(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<Color::IntType>(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::IntType>(move));
+    Color c(static_cast<Color::IntType>(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<typename T>
+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<bool, Board::max_pieces>& 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<array<bool, Board::max_pieces>> 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 (file)
index 0000000..f192b40
--- /dev/null
@@ -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 <QQmlListProperty>
+#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<PieceModel> pieceModels0 READ pieceModels0)
+    Q_PROPERTY(QQmlListProperty<PieceModel> pieceModels1 READ pieceModels1)
+    Q_PROPERTY(QQmlListProperty<PieceModel> pieceModels2 READ pieceModels2)
+    Q_PROPERTY(QQmlListProperty<PieceModel> 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<PieceModel> pieceModels0();
+
+    QQmlListProperty<PieceModel> pieceModels1();
+
+    QQmlListProperty<PieceModel> pieceModels2();
+
+    QQmlListProperty<PieceModel> 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<PieceModel*> m_pieceModels0;
+
+    QList<PieceModel*> m_pieceModels1;
+
+    QList<PieceModel*> m_pieceModels2;
+
+    QList<PieceModel*> 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<PieceModel*>& pieceModels);
+
+    bool findMove(const PieceModel& pieceModel, const QString& state,
+                  QPointF coord, Move& mv) const;
+
+    QList<PieceModel*>& 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<typename T>
+    void set(T& target, const T& value, void (GameModel::*changedSignal)(T));
+
+    PieceModel* updatePiece(Color c, Move mv,
+                            array<bool, Board::max_pieces>& 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 (file)
index 0000000..3153f11
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include <QApplication>
+#include <QCommandLineParser>
+#include <QMessageBox>
+#include <QTranslator>
+#include <QtQml>
+#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<GameModel>("pentobi", 1, 0, "GameModel");
+    qmlRegisterType<PlayerModel>("pentobi", 1, 0, "PlayerModel");
+    qmlRegisterInterface<PieceModel>("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>.", "n");
+    parser.addOption(optionSeed);
+    QCommandLineOption optionThreads("threads", "Use <n> 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 (file)
index 0000000..b4ba11f
--- /dev/null
@@ -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 (file)
index 0000000..8c255b0
--- /dev/null
@@ -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 <QDebug>
+#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<CoordPoint, 2 * PieceInfo::max_scored_size> 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<TransfTrigonIdentity>()
+                        : transforms.find<TransfIdentity>();
+    if (state == QLatin1String("rot60"))
+        return transforms.find<TransfTrigonRot60>();
+    if (state == QLatin1String("rot90"))
+        return transforms.find<TransfRectRot90>();
+    if (state == QLatin1String("rot120"))
+        return transforms.find<TransfTrigonRot120>();
+    if (state == QLatin1String("rot180"))
+        return isTrigon ? transforms.find<TransfTrigonRot180>()
+                        : transforms.find<TransfRectRot180>();
+    if (state == QLatin1String("rot240"))
+        return transforms.find<TransfTrigonRot240>();
+    if (state == QLatin1String("rot270"))
+        return transforms.find<TransfRectRot270>();
+    if (state == QLatin1String("rot300"))
+        return transforms.find<TransfTrigonRot300>();
+    if (state == QLatin1String("flip"))
+        return isTrigon ? transforms.find<TransfTrigonReflRot180>()
+                        : transforms.find<TransfRectRot180Refl>();
+    if (state == QLatin1String("rot60Flip"))
+        return transforms.find<TransfTrigonReflRot120>();
+    if (state == QLatin1String("rot90Flip"))
+        return transforms.find<TransfRectRot90Refl>();
+    if (state == QLatin1String("rot120Flip"))
+        return transforms.find<TransfTrigonReflRot60>();
+    if (state == QLatin1String("rot180Flip"))
+        return isTrigon ? transforms.find<TransfTrigonRefl>()
+                        : transforms.find<TransfRectRefl>();
+    if (state == QLatin1String("rot240Flip"))
+        return transforms.find<TransfTrigonReflRot300>();
+    if (state == QLatin1String("rot270Flip"))
+        return transforms.find<TransfRectRot270Refl>();
+    if (state == QLatin1String("rot300Flip"))
+        return transforms.find<TransfTrigonReflRot240>();
+    qWarning() << "PieceModel: unknown state " << m_state;
+    return transforms.find<TransfIdentity>();
+}
+
+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<qreal>(p.y) + 1.f / 3;
+            else
+                centerY = static_cast<qreal>(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<const TransfIdentity*>(transform)
+            || dynamic_cast<const TransfTrigonIdentity*>(transform))
+        ;
+    else if (dynamic_cast<const TransfTrigonRot60*>(transform))
+        state = QLatin1String("rot60");
+    else if (dynamic_cast<const TransfRectRot90*>(transform))
+        state = QLatin1String("rot90");
+    else if (dynamic_cast<const TransfTrigonRot120*>(transform))
+        state = QLatin1String("rot120");
+    else if (dynamic_cast<const TransfRectRot180*>(transform)
+             || dynamic_cast<const TransfTrigonRot180*>(transform))
+        state = QLatin1String("rot180");
+    else if (dynamic_cast<const TransfTrigonRot240*>(transform))
+        state = QLatin1String("rot240");
+    else if (dynamic_cast<const TransfRectRot270*>(transform))
+        state = QLatin1String("rot270");
+    else if (dynamic_cast<const TransfTrigonRot300*>(transform))
+        state = QLatin1String("rot300");
+    else if (dynamic_cast<const TransfRectRot180Refl*>(transform)
+             || dynamic_cast<const TransfTrigonReflRot180*>(transform))
+        state = QLatin1String("flip");
+    else if (dynamic_cast<const TransfTrigonReflRot120*>(transform))
+        state = QLatin1String("rot60Flip");
+    else if (dynamic_cast<const TransfRectRot90Refl*>(transform))
+        state = QLatin1String("rot90Flip");
+    else if (dynamic_cast<const TransfTrigonReflRot60*>(transform))
+        state = QLatin1String("rot120Flip");
+    else if (dynamic_cast<const TransfRectRefl*>(transform)
+             || dynamic_cast<const TransfTrigonRefl*>(transform))
+        state = QLatin1String("rot180Flip");
+    else if (dynamic_cast<const TransfTrigonReflRot300*>(transform))
+        state = QLatin1String("rot240Flip");
+    else if (dynamic_cast<const TransfRectRot270Refl*>(transform))
+        state = QLatin1String("rot270Flip");
+    else if (dynamic_cast<const TransfTrigonReflRot240*>(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 (file)
index 0000000..dc344fd
--- /dev/null
@@ -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 <QObject>
+#include <QPointF>
+#include <QVariant>
+#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 (file)
index 0000000..a21cb98
--- /dev/null
@@ -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 <QElapsedTimer>
+#include <QFile>
+#include <QtConcurrentRun>
+#include <QSettings>
+
+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<GenMoveResult> 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 (file)
index 0000000..b08cebe
--- /dev/null
@@ -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 <QFutureWatcher>
+#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<GenMoveResult> 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 (file)
index 0000000..60bb1c7
--- /dev/null
@@ -0,0 +1,49 @@
+<?xml version="1.0"?>
+<manifest package="net.sf.pentobi" xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="12.2" android:versionCode="12002" android:installLocation="auto">
+    <application android:hardwareAccelerated="true" android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="Pentobi" android:theme="@style/AppTheme" android:icon="@drawable/icon">
+        <activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|locale|fontScale|keyboard|keyboardHidden|navigation" android:name="org.qtproject.qt5.android.bindings.QtActivity" android:label="@string/app_name" android:screenOrientation="portrait" android:launchMode="singleTop">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+            <meta-data android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --"/>
+            <meta-data android:name="android.app.qt_sources_resource_id" android:resource="@array/qt_sources"/>
+            <meta-data android:name="android.app.repository" android:value="default"/>
+            <meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/>
+            <meta-data android:name="android.app.bundled_libs_resource_id" android:resource="@array/bundled_libs"/>
+            <!-- Deploy Qt libs as part of package -->
+            <meta-data android:name="android.app.bundle_local_qt_libs" android:value="-- %%BUNDLE_LOCAL_QT_LIBS%% --"/>
+            <meta-data android:name="android.app.bundled_in_lib_resource_id" android:resource="@array/bundled_in_lib"/>
+            <meta-data android:name="android.app.bundled_in_assets_resource_id" android:resource="@array/bundled_in_assets"/>
+            <!-- Run with local libs -->
+            <meta-data android:name="android.app.use_local_qt_libs" android:value="-- %%USE_LOCAL_QT_LIBS%% --"/>
+            <meta-data android:name="android.app.libs_prefix" android:value="/data/local/tmp/qt/"/>
+            <meta-data android:name="android.app.load_local_libs" android:value="-- %%INSERT_LOCAL_LIBS%% --"/>
+            <meta-data android:name="android.app.load_local_jars" android:value="-- %%INSERT_LOCAL_JARS%% --"/>
+            <meta-data android:name="android.app.static_init_classes" android:value="-- %%INSERT_INIT_CLASSES%% --"/>
+            <!--  Messages maps -->
+            <meta-data android:value="@string/ministro_not_found_msg" android:name="android.app.ministro_not_found_msg"/>
+            <meta-data android:value="@string/ministro_needed_msg" android:name="android.app.ministro_needed_msg"/>
+            <meta-data android:value="@string/fatal_error_msg" android:name="android.app.fatal_error_msg"/>
+            <!--  Messages maps -->
+
+            <!-- Splash screen -->
+            <meta-data android:name="android.app.splash_screen_drawable" android:resource="@drawable/splash"/>
+            <!-- Splash screen -->
+        </activity>
+    </application>
+    <uses-sdk android:minSdkVersion="9" android:targetSdkVersion="9"/>
+    <supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/>
+
+    <!-- The following comment will be replaced upon deployment with default permissions based on the dependencies of the application.
+         Remove the comment if you do not require these default permissions. -->
+
+
+    <!-- The following comment will be replaced upon deployment with default features based on the dependencies of the application.
+         Remove the comment if you do not require these default features. -->
+
+
+<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+
+
+</manifest>
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 (file)
index 0000000..4a5bd8e
Binary files /dev/null and b/src/pentobi_qml/android/res/drawable-hdpi/icon.png differ
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 (file)
index 0000000..4fb4397
Binary files /dev/null and b/src/pentobi_qml/android/res/drawable-mdpi/icon.png differ
diff --git a/src/pentobi_qml/android/res/drawable/splash.xml b/src/pentobi_qml/android/res/drawable/splash.xml
new file mode 100644 (file)
index 0000000..32f67f9
--- /dev/null
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape android:shape="rectangle" >
+            <solid android:color="#131313"/>
+        </shape>
+    </item>
+    <item>
+         <bitmap android:src="@drawable/icon" android:gravity="center" />
+    </item>
+</layer-list>
diff --git a/src/pentobi_qml/android/res/values/theme.xml b/src/pentobi_qml/android/res/values/theme.xml
new file mode 100644 (file)
index 0000000..adec232
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <style name="AppTheme" parent="@android:style/Theme.DeviceDefault.Light.NoActionBar">
+        <item name="android:windowBackground">@drawable/splash</item>
+    </style>
+</resources>
diff --git a/src/pentobi_qml/android_icons_svg/icon48.svg b/src/pentobi_qml/android_icons_svg/icon48.svg
new file mode 100644 (file)
index 0000000..655a58e
--- /dev/null
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="48" width="48" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<g id="e" transform="matrix(1.099991 0 0 1.099983 18.49986 -17.799)">
+<rect opacity="0.99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#c00"/>
+<path fill-opacity=".1568628" d="m-15 28 0.909098-0.909578h8.181886v-8.181946l0.9093462-0.909106v10z"/>
+<path d="m-15 28v-10h10l-0.9090162 0.908476h-8.181886v8.181946z" fill-opacity=".1568628" fill="#fff"/>
+</g>
+<use xlink:href="#e" transform="translate(10.99991)" height="48" width="48" y="0" x="0"/>
+<use xlink:href="#e" transform="translate(10.99991 10.99983)" height="48" width="48" y="0" x="0"/>
+<use xlink:href="#e" transform="translate(21.99982 10.99983)" height="48" width="48" y="0" x="0"/>
+<g id="f" transform="matrix(1.099991 0 0 1.099983 18.49986 -6.79917)">
+<rect opacity="0.99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#edd400"/>
+<path fill-opacity=".1568628" d="m-15 28 0.909098-0.90942h8.181886v-8.181947l0.9093462-0.909263v10z"/>
+<path d="m-15 28v-10h10l-0.9090162 0.908633h-8.181886v8.181947z" fill-opacity=".1568628" fill="#fff"/>
+</g>
+<use xlink:href="#f" transform="translate(1.222212e-8 10.99983)" height="72" width="72" y="0" x="0"/>
+<use xlink:href="#f" transform="translate(1.222212e-8 21.99965)" height="72" width="72" y="0" x="0"/>
+<use xlink:href="#f" transform="translate(10.99991 21.99965)" height="72" width="72" y="0" x="0"/>
+<g id="g" transform="matrix(1.099991 0 0 1.099983 29.49977 4.200657)">
+<rect opacity="0.99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#3465a4"/>
+<path fill-opacity=".1568628" d="m-15 28 0.909181-0.909262h8.181886v-8.181947l0.9092634-0.909421v10z"/>
+<path d="m-15 28v-10h10l-0.9089334 0.908791h-8.181886v8.181947z" fill-opacity=".1568628" fill="#fff"/>
+</g>
+<use xlink:href="#g" transform="translate(10.99991 10.99983)" height="72" width="72" y="0" x="0"/>
+<use xlink:href="#g" transform="translate(10.99991)" height="72" width="72" y="0" x="0"/>
+<use xlink:href="#g" transform="translate(21.99982 10.99983)" height="72" width="72" y="0" x="0"/>
+<g id="h" transform="matrix(1.099991 0 0 1.099983 40.49968 -17.799)">
+<rect opacity="0.99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#73d216"/>
+<path fill-opacity=".1568628" d="m-15 28 0.909263-0.909578h8.181886v-8.181946l0.9091807-0.909106v10z"/>
+<path d="m-15 28v-10h10l-0.9088507 0.908476h-8.181886v8.181946z" fill-opacity=".1568628" fill="#fff"/>
+</g>
+<use xlink:href="#h" transform="translate(10.99991)" height="72" width="72" y="0" x="0"/>
+<use xlink:href="#h" transform="translate(10.99991 10.99983)" height="72" width="72" y="0" x="0"/>
+<use xlink:href="#h" transform="translate(10.99991 21.99965)" height="72" width="72" y="0" x="0"/>
+</svg>
diff --git a/src/pentobi_qml/android_icons_svg/icon72.svg b/src/pentobi_qml/android_icons_svg/icon72.svg
new file mode 100644 (file)
index 0000000..3575ed4
--- /dev/null
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="72" width="72" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<g id="e" transform="matrix(1.699996 0 0 1.699996 27.49994 -28.59993)">
+<rect opacity="0.99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#c00"/>
+<path fill-opacity=".1568628" d="m-15 28 1.176473-1.176448h7.647078v-7.647078l1.176549-1.176374v10z"/>
+<path d="m-15 28v-10h10l-1.176449 1.176474h-7.647078v7.647078z" fill-opacity=".1568628" fill="#fff"/>
+</g>
+<use xlink:href="#e" transform="translate(16.99996 2.0042e-7)" height="48" width="48" y="0" x="0"/>
+<use xlink:href="#e" transform="translate(16.99996 16.99996)" height="48" width="48" y="0" x="0"/>
+<use xlink:href="#e" transform="translate(33.99992 16.99996)" height="48" width="48" y="0" x="0"/>
+<g id="f" transform="matrix(1.699996 0 0 1.699996 27.49994 -11.59997)">
+<rect opacity="0.99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#edd400"/>
+<path fill-opacity=".1568628" d="m-15 28 1.176473-1.176423h7.647078v-7.647078l1.176549-1.176399v10z"/>
+<path d="m-15 28v-10h10l-1.176449 1.176499h-7.647078v7.647078z" fill-opacity=".1568628" fill="#fff"/>
+</g>
+<use xlink:href="#f" transform="translate(1.888884e-8 16.99996)" height="72" width="72" y="0" x="0"/>
+<use xlink:href="#f" transform="translate(1.888884e-8 33.99992)" height="72" width="72" y="0" x="0"/>
+<use xlink:href="#f" transform="translate(16.99996 33.99992)" height="72" width="72" y="0" x="0"/>
+<g id="g" transform="matrix(1.699996 0 0 1.699996 44.49989 5.39999)">
+<rect opacity="0.99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#3465a4"/>
+<path fill-opacity=".1568628" d="m-15 28 1.176498-1.176398h7.647078v-7.647078l1.176524-1.176424v10z"/>
+<path d="m-15 28v-10h10l-1.176424 1.176524h-7.647078v7.647078z" fill-opacity=".1568628" fill="#fff"/>
+</g>
+<use xlink:href="#g" transform="translate(16.99996 16.99996)" height="72" width="72" y="0" x="0"/>
+<use xlink:href="#g" transform="translate(16.99996 4.0042e-7)" height="72" width="72" y="0" x="0"/>
+<use xlink:href="#g" transform="translate(33.99992 16.99996)" height="72" width="72" y="0" x="0"/>
+<g id="h" transform="matrix(1.699996 0 0 1.699996 61.49985 -28.59993)">
+<rect opacity="0.99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#73d216"/>
+<path fill-opacity=".1568628" d="m-15 28 1.176523-1.176448h7.647078v-7.647078l1.176499-1.176374v10z"/>
+<path d="m-15 28v-10h10l-1.176399 1.176474h-7.647078v7.647078z" fill-opacity=".1568628" fill="#fff"/>
+</g>
+<use xlink:href="#h" transform="translate(16.99996 2.0042e-7)" height="72" width="72" y="0" x="0"/>
+<use xlink:href="#h" transform="translate(16.99996 16.99996)" height="72" width="72" y="0" x="0"/>
+<use xlink:href="#h" transform="translate(16.99996 33.99992)" height="72" width="72" y="0" x="0"/>
+</svg>
diff --git a/src/pentobi_qml/deployment.pri b/src/pentobi_qml/deployment.pri
new file mode 100644 (file)
index 0000000..5441b63
--- /dev/null
@@ -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 (file)
index 0000000..c3a99c7
--- /dev/null
@@ -0,0 +1,15 @@
+<RCC>
+    <qresource prefix="/">
+        <file>qml/icons/menu.svg</file>
+        <file>qml/icons/pentobi-backward.svg</file>
+        <file>qml/icons/pentobi-beginning.svg</file>
+        <file>qml/icons/pentobi-computer-colors.svg</file>
+        <file>qml/icons/pentobi-end.svg</file>
+        <file>qml/icons/pentobi-forward.svg</file>
+        <file>qml/icons/pentobi-newgame.svg</file>
+        <file>qml/icons/pentobi-next-variation.svg</file>
+        <file>qml/icons/pentobi-play.svg</file>
+        <file>qml/icons/pentobi-previous-variation.svg</file>
+        <file>qml/icons/pentobi-undo.svg</file>
+    </qresource>
+</RCC>
diff --git a/src/pentobi_qml/qml/.gitignore b/src/pentobi_qml/qml/.gitignore
new file mode 100644 (file)
index 0000000..8df47d5
--- /dev/null
@@ -0,0 +1 @@
+*.qm
diff --git a/src/pentobi_qml/qml/AndroidToolBar.qml b/src/pentobi_qml/qml/AndroidToolBar.qml
new file mode 100644 (file)
index 0000000..f0bf836
--- /dev/null
@@ -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 (file)
index 0000000..44f21b3
--- /dev/null
@@ -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 (file)
index 0000000..3e818d9
--- /dev/null
@@ -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 (file)
index 0000000..172c5b0
--- /dev/null
@@ -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 (file)
index 0000000..db18fcd
--- /dev/null
@@ -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 (file)
index 0000000..58e3d0f
--- /dev/null
@@ -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 (file)
index 0000000..412dd68
--- /dev/null
@@ -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 (file)
index 0000000..b309853
--- /dev/null
@@ -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 (file)
index 0000000..d6a3963
--- /dev/null
@@ -0,0 +1,274 @@
+function about() {
+    var url = "http://pentobi.sourceforge.net"
+    showInfo("<h2>" + qsTr("Pentobi") + "</h2><p>" +
+             qsTr("Version %1").arg(Qt.application.version) + "</p><p>" +
+             qsTr("Computer opponent for the board game Blokus.") + "<br>" +
+             qsTr("&copy; 2011&ndash;%1 Markus&nbsp;Enzenberger").arg(2017) +
+             "<br><a href=\"" + url + "\">" + url + "</a></p>")
+}
+
+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 (file)
index 0000000..a90ba50
--- /dev/null
@@ -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 (file)
index 0000000..6299b2b
--- /dev/null
@@ -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 (file)
index 0000000..cacfaa1
--- /dev/null
@@ -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 (file)
index 0000000..7ebd92e
--- /dev/null
@@ -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 (file)
index 0000000..253ff0f
--- /dev/null
@@ -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 (file)
index 0000000..d26ea34
--- /dev/null
@@ -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 (file)
index 0000000..6999084
--- /dev/null
@@ -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 (file)
index 0000000..6c3c9c1
--- /dev/null
@@ -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 (file)
index 0000000..cec07f5
--- /dev/null
@@ -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 (file)
index 0000000..4db4d53
--- /dev/null
@@ -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 (file)
index 0000000..c9076c7
--- /dev/null
@@ -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 (file)
index 0000000..9f635d8
--- /dev/null
@@ -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 (file)
index 0000000..afa9041
--- /dev/null
@@ -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 (file)
index 0000000..9c43deb
--- /dev/null
@@ -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 (file)
index 0000000..4caa196
--- /dev/null
@@ -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 (file)
index 0000000..9a06b5b
--- /dev/null
@@ -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 (file)
index 0000000..b5b7d0a
--- /dev/null
@@ -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 (file)
index 0000000..0f2c268
--- /dev/null
@@ -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 (file)
index 0000000..30c9f66
--- /dev/null
@@ -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 (file)
index 0000000..91436ea
--- /dev/null
@@ -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 (file)
index 0000000..1a59687
--- /dev/null
@@ -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 (file)
index 0000000..7e842a1
--- /dev/null
@@ -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 (file)
index 0000000..1211cf4
--- /dev/null
@@ -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 ? "*" : "") + "<u>" + value + "</u>"
+        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 (file)
index 0000000..436c15a
--- /dev/null
@@ -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 ? "(<u>" + value + "</u>)" : "(" + value + ")"
+            else
+                return isFinal ? "<u>" + value + "</u>" : 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 (file)
index 0000000..a2901ec
--- /dev/null
@@ -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 (file)
index 0000000..d478f6a
--- /dev/null
@@ -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 (file)
index 0000000..79083c8
--- /dev/null
@@ -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 (file)
index 0000000..9e7af56
--- /dev/null
@@ -0,0 +1,480 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="de_DE">
+<context>
+    <name>ComputerColorDialog</name>
+    <message>
+        <source>Computer Colors</source>
+        <translation>Computer-Farben</translation>
+    </message>
+    <message>
+        <source>Computer plays:</source>
+        <translation>Computer spielt:</translation>
+    </message>
+    <message>
+        <source>Blue/Red</source>
+        <translation>Blau/Rot</translation>
+    </message>
+    <message>
+        <source>Blue</source>
+        <translation>Blau</translation>
+    </message>
+    <message>
+        <source>Yellow/Green</source>
+        <translation>Gelb/Grün</translation>
+    </message>
+    <message>
+        <source>Green</source>
+        <translation>Grün</translation>
+    </message>
+    <message>
+        <source>Yellow</source>
+        <translation>Gelb</translation>
+    </message>
+    <message>
+        <source>Red</source>
+        <translation>Rot</translation>
+    </message>
+</context>
+<context>
+    <name>GameModel</name>
+    <message>
+        <source>(Setup)</source>
+        <translation>(Stellung)</translation>
+    </message>
+    <message>
+        <source>(No moves)</source>
+        <translation>(Keine Züge)</translation>
+    </message>
+    <message>
+        <source>Move %1</source>
+        <translation>Zug %1</translation>
+    </message>
+    <message>
+        <source>Blue wins with 1 point.</source>
+        <translation>Blau gewinnt mit 1 Punkt.</translation>
+    </message>
+    <message>
+        <source>Blue wins with %1 points.</source>
+        <translation>Blau gewinnt mit %1 Punkten.</translation>
+    </message>
+    <message>
+        <source>Green wins with 1 point.</source>
+        <translation>Grün gewinnt mit 1 Punkt.</translation>
+    </message>
+    <message>
+        <source>Green wins with %1 points.</source>
+        <translation>Grün gewinnt mit %1 Punkten.</translation>
+    </message>
+    <message>
+        <source>Green wins (tie resolved).</source>
+        <translation>Grün gewinnt (Unentschieden aufgelöst).</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie.</source>
+        <translation>Spiel endet unentschieden.</translation>
+    </message>
+    <message>
+        <source>Blue/Red wins with 1 point.</source>
+        <translation>Blau/Rot gewinnt mit 1 Punkt.</translation>
+    </message>
+    <message>
+        <source>Blue/Red wins with %1 points.</source>
+        <translation>Blau/Rot gewinnt mit %1 Punkten.</translation>
+    </message>
+    <message>
+        <source>Yellow/Green wins with 1 point.</source>
+        <translation>Gelb/Grün gewinnt mit 1 Punkt.</translation>
+    </message>
+    <message>
+        <source>Yellow/Green wins with %1 points.</source>
+        <translation>Gelb/Grün gewinnt mit %1 Punkten.</translation>
+    </message>
+    <message>
+        <source>Yellow/Green wins (tie resolved).</source>
+        <translation>Gelb/Grün gewinnt (Unentschieden aufgelöst).</translation>
+    </message>
+    <message>
+        <source>Blue wins.</source>
+        <translation>Blau gewinnt.</translation>
+    </message>
+    <message>
+        <source>Yellow wins.</source>
+        <translation>Gelb gewinnt.</translation>
+    </message>
+    <message>
+        <source>Red wins.</source>
+        <translation>Rot gewinnt.</translation>
+    </message>
+    <message>
+        <source>Red wins (tie resolved).</source>
+        <translation>Rot gewinnt (Unentschieden aufgelöst).</translation>
+    </message>
+    <message>
+        <source>Yellow wins (tie resolved).</source>
+        <translation>Gelb gewinnt (Unentschieden aufgelöst).</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue and Yellow.</source>
+        <translation>Spiel endet in einem Unentschieden zwischen Blau und Gelb.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue and Red.</source>
+        <translation>Spiel endet in einem Unentschieden zwischen Blau und Rot.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Yellow and Red.</source>
+        <translation>Spiel endet in einem Unentschieden zwischen Gelb und Rot.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between all players.</source>
+        <translation>Spiel endet in einem Unentschieden zwischen allen Spielern.</translation>
+    </message>
+    <message>
+        <source>Green wins.</source>
+        <translation>Grün gewinnt.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue, Yellow and Red.</source>
+        <translation>Spiel endet in einem Unentschieden zwischen Blau, Gelb und Rot.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue, Yellow and Green.</source>
+        <translation>Spiel endet in einem Unentschieden zwischen Blau, Gelb und Grün.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue, Red and Green.</source>
+        <translation>Spiel endet in einem Unentschieden zwischen Blau, Rot und Grün.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Yellow, Red and Green.</source>
+        <translation>Spiel endet in einem Unentschieden zwischen Gelb, Rot und Grün.</translation>
+    </message>
+</context>
+<context>
+    <name>Main</name>
+    <message>
+        <source>Pentobi</source>
+        <translation>Pentobi</translation>
+    </message>
+    <message>
+        <source>New game?</source>
+        <translation>Neues Spiel?</translation>
+    </message>
+    <message>
+        <source>Open failed.</source>
+        <translation>Öffnen fehlgeschlagen.</translation>
+    </message>
+    <message>
+        <source>Save failed.</source>
+        <translation>Speichern fehlgeschlagen.</translation>
+    </message>
+    <message>
+        <source>Truncate this subtree?</source>
+        <translation>Diesen Teilbaum abschneiden?</translation>
+    </message>
+    <message>
+        <source>Truncate children?</source>
+        <translation>Kindknoten abschneiden?</translation>
+    </message>
+    <message>
+        <source>Delete all variations?</source>
+        <translation>Alle Varianten löschen?</translation>
+    </message>
+    <message>
+        <source>Version %1</source>
+        <translation>Version %1</translation>
+    </message>
+    <message>
+        <source>Computer opponent for the board game Blokus.</source>
+        <translation>Computer-Gegner für das Brettspiel Blokus.</translation>
+    </message>
+    <message>
+        <source>&amp;copy; 2011&amp;ndash;%1 Markus&amp;nbsp;Enzenberger</source>
+        <translation>&amp;copy; 2011&amp;ndash;%1 Markus&amp;nbsp;Enzenberger</translation>
+    </message>
+</context>
+<context>
+    <name>MenuComputer</name>
+    <message>
+        <source>&amp;Computer</source>
+        <translation>&amp;Computer</translation>
+    </message>
+    <message>
+        <source>Computer &amp;Colors</source>
+        <translation>Computer-&amp;Farben</translation>
+    </message>
+    <message>
+        <source>&amp;Play</source>
+        <translation>&amp;Spielen</translation>
+    </message>
+    <message>
+        <source>&amp;Level (Classic, 4 Players)</source>
+        <translation>Spielst&amp;ufe (Klassisch, 4 Spieler)</translation>
+    </message>
+    <message>
+        <source>&amp;Level (Classic, 2 Players)</source>
+        <translation>Spielst&amp;ufe (Klassisch, 2 Spieler)</translation>
+    </message>
+    <message>
+        <source>&amp;Level (Classic, 3 Players)</source>
+        <translation>Spielst&amp;ufe (Klassisch, 3 Spieler)</translation>
+    </message>
+    <message>
+        <source>&amp;Level (Duo)</source>
+        <translation>Spielst&amp;ufe (Duo)</translation>
+    </message>
+    <message>
+        <source>&amp;Level (Junior)</source>
+        <translation>Spielst&amp;ufe (Junior)</translation>
+    </message>
+    <message>
+        <source>&amp;Level (Trigon, 4 Players)</source>
+        <translation>Spielst&amp;ufe (Trigon, 4 Spieler)</translation>
+    </message>
+    <message>
+        <source>&amp;Level (Trigon, 2 Players)</source>
+        <translation>Spielst&amp;ufe (Trigon, 2 Spieler)</translation>
+    </message>
+    <message>
+        <source>&amp;Level (Trigon, 3 Players)</source>
+        <translation>Spielst&amp;ufe (Trigon, 3 Spieler)</translation>
+    </message>
+    <message>
+        <source>&amp;Level (Nexos, 4 Players)</source>
+        <translation>Spielst&amp;ufe (Nexos, 4 Spieler)</translation>
+    </message>
+    <message>
+        <source>&amp;Level (Nexos, 2 Players)</source>
+        <translation>Spielst&amp;ufe (Nexos, 2 Spieler)</translation>
+    </message>
+    <message>
+        <source>&amp;Level (Callisto, 4 Players)</source>
+        <translation>Spielst&amp;ufe (Callisto, 4 Spieler)</translation>
+    </message>
+    <message>
+        <source>&amp;Level (Callisto, 2 Players)</source>
+        <translation>Spielst&amp;ufe (Callisto, 2 Spieler)</translation>
+    </message>
+    <message>
+        <source>&amp;Level (Callisto, 3 Players)</source>
+        <translation>Spielst&amp;ufe (Callisto, 3 Spieler)</translation>
+    </message>
+</context>
+<context>
+    <name>MenuEdit</name>
+    <message>
+        <source>&amp;Edit</source>
+        <translation>&amp;Bearbeiten</translation>
+    </message>
+    <message>
+        <source>Make &amp;Main Variation</source>
+        <translation>Zu &amp;Hauptvariante machen</translation>
+    </message>
+    <message>
+        <source>Move Variation &amp;Up</source>
+        <translation>Variante nach &amp;oben schieben</translation>
+    </message>
+    <message>
+        <source>Move Variation &amp;Down</source>
+        <translation>Variante nach &amp;unten schieben</translation>
+    </message>
+    <message>
+        <source>&amp;Truncate</source>
+        <translation>&amp;Abschneiden</translation>
+    </message>
+    <message>
+        <source>Truncate &amp;Children</source>
+        <translation>&amp;Kindknoten abschneiden</translation>
+    </message>
+    <message>
+        <source>&amp;Delete All Variations</source>
+        <translation>Alle &amp;Varianten löschen</translation>
+    </message>
+    <message>
+        <source>&amp;Next Color</source>
+        <translation>&amp;Nächste Farbe</translation>
+    </message>
+</context>
+<context>
+    <name>MenuGame</name>
+    <message>
+        <source>&amp;Game</source>
+        <translation>&amp;Spiel</translation>
+    </message>
+    <message>
+        <source>&amp;New</source>
+        <translation>&amp;Neu</translation>
+    </message>
+    <message>
+        <source>Game &amp;Variant</source>
+        <translation>Spiel&amp;variante</translation>
+    </message>
+    <message>
+        <source>Classic (&amp;3 Players)</source>
+        <translation>Klassisch (&amp;3 Spieler)</translation>
+    </message>
+    <message>
+        <source>Classic (&amp;4 Players)</source>
+        <translation>Klassisch (&amp;4 Spieler)</translation>
+    </message>
+    <message>
+        <source>&amp;Duo</source>
+        <translation>&amp;Duo</translation>
+    </message>
+    <message>
+        <source>&amp;Junior</source>
+        <translation>&amp;Junior</translation>
+    </message>
+    <message>
+        <source>&amp;Undo Move</source>
+        <translation>Zug &amp;rückgängig</translation>
+    </message>
+    <message>
+        <source>&amp;Find Move</source>
+        <translation>Zug &amp;finden</translation>
+    </message>
+    <message>
+        <source>&amp;Open...</source>
+        <translation>Öffn&amp;en ...</translation>
+    </message>
+    <message>
+        <source>&amp;Save As...</source>
+        <translation>&amp;Speichern unter ...</translation>
+    </message>
+    <message>
+        <source>&amp;Quit</source>
+        <translation>&amp;Beenden</translation>
+    </message>
+    <message>
+        <source>&amp;Classic</source>
+        <translation>&amp;Klassisch</translation>
+    </message>
+    <message>
+        <source>Classic (&amp;2 Players)</source>
+        <translation>Klassisch (&amp;2 Spieler)</translation>
+    </message>
+    <message>
+        <source>&amp;Trigon</source>
+        <translation>&amp;Trigon</translation>
+    </message>
+    <message>
+        <source>Trigon (&amp;2 Players)</source>
+        <translation>Trigon (&amp;2 Spieler)</translation>
+    </message>
+    <message>
+        <source>Trigon (&amp;3 Players)</source>
+        <translation>Trigon (&amp;3 Spieler)</translation>
+    </message>
+    <message>
+        <source>Trigon (&amp;4 Players)</source>
+        <translation>Trigon (&amp;4 Spieler)</translation>
+    </message>
+    <message>
+        <source>&amp;Nexos</source>
+        <translation>&amp;Nexos</translation>
+    </message>
+    <message>
+        <source>Nexos (&amp;2 Players)</source>
+        <translation>Nexos (&amp;2 Spieler)</translation>
+    </message>
+    <message>
+        <source>Nexos (&amp;4 Players)</source>
+        <translation>Nexos (&amp;4 Spieler)</translation>
+    </message>
+    <message>
+        <source>C&amp;allisto</source>
+        <translation>&amp;Callisto</translation>
+    </message>
+    <message>
+        <source>Callisto (&amp;2 Players)</source>
+        <translation>Callisto (&amp;2 Spieler)</translation>
+    </message>
+    <message>
+        <source>Callisto (&amp;3 Players)</source>
+        <translation>Callisto (&amp;3 Spieler)</translation>
+    </message>
+    <message>
+        <source>Callisto (&amp;4 Players)</source>
+        <translation>Callisto (&amp;4 Spieler)</translation>
+    </message>
+</context>
+<context>
+    <name>MenuGo</name>
+    <message>
+        <source>G&amp;o</source>
+        <translation>&amp;Gehe zu</translation>
+    </message>
+    <message>
+        <source>Back to &amp;Main Variation</source>
+        <translation>Zurück zu &amp;Hauptvariante</translation>
+    </message>
+</context>
+<context>
+    <name>MenuHelp</name>
+    <message>
+        <source>&amp;Help</source>
+        <translation>&amp;Hilfe</translation>
+    </message>
+    <message>
+        <source>&amp;About Pentobi</source>
+        <translation>Über &amp;Pentobi</translation>
+    </message>
+</context>
+<context>
+    <name>MenuView</name>
+    <message>
+        <source>&amp;View</source>
+        <translation>&amp;Ansicht</translation>
+    </message>
+    <message>
+        <source>Mark &amp;Last Move</source>
+        <translation>&amp;Letzten Zug markieren</translation>
+    </message>
+    <message>
+        <source>&amp;Animate Pieces</source>
+        <translation>Spielsteine &amp;animieren</translation>
+    </message>
+</context>
+<context>
+    <name>OpenDialog</name>
+    <message>
+        <source>Open</source>
+        <translation>Öffnen</translation>
+    </message>
+    <message>
+        <source>Blokus games (*.blksgf)</source>
+        <translation>Blokus-Partien (*.blksgf)</translation>
+    </message>
+    <message>
+        <source>All files (*)</source>
+        <translation>Alle Dateien (*)</translation>
+    </message>
+</context>
+<context>
+    <name>SaveDialog</name>
+    <message>
+        <source>Save</source>
+        <translation>Speichern</translation>
+    </message>
+    <message>
+        <source>Blokus games (*.blksgf)</source>
+        <translation>Blokus-Partien (*.blksgf)</translation>
+    </message>
+    <message>
+        <source>All files (*)</source>
+        <translation>Alle Dateien (*)</translation>
+    </message>
+</context>
+<context>
+    <name>main</name>
+    <message>
+        <source>Not enough memory.</source>
+        <translation>Nicht genügend Speicher.</translation>
+    </message>
+    <message>
+        <source>Pentobi</source>
+        <translation>Pentobi</translation>
+    </message>
+</context>
+</TS>
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 (file)
index 0000000..0fd0c68
--- /dev/null
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="de_DE">
+<context>
+    <name>QPlatformTheme</name>
+    <message>
+        <source>Cancel</source>
+        <translation>Abbrechen</translation>
+    </message>
+</context>
+</TS>
diff --git a/src/pentobi_qml/qml/icons/menu.svg b/src/pentobi_qml/qml/icons/menu.svg
new file mode 100644 (file)
index 0000000..6aed026
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect id="a" height="4" width="4" y="3" x="9" fill="#888"/>
+<use xlink:href="#a" y="6"/>
+<use xlink:href="#a" y="12"/>
+</svg>
diff --git a/src/pentobi_qml/qml/icons/pentobi-backward.svg b/src/pentobi_qml/qml/icons/pentobi-backward.svg
new file mode 100644 (file)
index 0000000..993c75d
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m10.5 2.5s0 2.5 0 4h10v9h-10c0 1.5 0 4 0 4l-8.5-8.5 8.5-8.5z" fill="#888"/>
+</svg>
diff --git a/src/pentobi_qml/qml/icons/pentobi-beginning.svg b/src/pentobi_qml/qml/icons/pentobi-beginning.svg
new file mode 100644 (file)
index 0000000..baf6c04
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m4.5 2.5v8l4-4v-0h0l4-4s0 2.5 0 4l9 0v9l-9 0c0 1.5 0 4 0 4l-4-4l-4-4v8h-4v-17z" fill="#888"/>
+</svg>
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 (file)
index 0000000..8e7c890
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <path fill="#888" d="m2.5 0c-1.35 0-2.5 1.1-2.5 2.44v15.1c0 1.35 1.1 2.44 2.44 2.44h7.56v2.02h12v-19.6c0-1.27-1.2-2.3-2.5-2.3zm16 3c0.3 0 0.522 0.258 0.522 0.562v6.44h-1v-6h-14v11h6v1l-6.42 0.002c-0.3 0-0.56-0.3-0.56-0.6v-11.9c0-0.26 0.18-0.5 0.48-0.5zm-7.5 8h10v10h-10zm1 1v4h4v-4zm4 4v4h4v-4zm-13 1h7v1h-7z"/>
+</svg>
diff --git a/src/pentobi_qml/qml/icons/pentobi-end.svg b/src/pentobi_qml/qml/icons/pentobi-end.svg
new file mode 100644 (file)
index 0000000..7b4349e
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path fill="#888" d="m17.5 2.5v8l-4-4l-4-4s0 2.5 0 4l-9 0v9l9 0c0 1.5 0 4 0 4l4-4l4-4v8h4v-17z"/>
+</svg>
diff --git a/src/pentobi_qml/qml/icons/pentobi-forward.svg b/src/pentobi_qml/qml/icons/pentobi-forward.svg
new file mode 100644 (file)
index 0000000..1fe5404
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path fill="#888" d="m12.5 2.5s0 2.5 0 4h-10v9h10c0 1.5 0 4 0 4l8.5-8.5-8.5-8.5z"/>
+</svg>
diff --git a/src/pentobi_qml/qml/icons/pentobi-newgame.svg b/src/pentobi_qml/qml/icons/pentobi-newgame.svg
new file mode 100644 (file)
index 0000000..202a885
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <path fill="#888" d="m2 0c-1.11 0-2 0.894-2 2v17c0 1.11 0.892 2 2 2h17c1.11 0 2-0.894 2-2v-17c0-1.15-0.9-2-2-2-5.6 0-11.3 0-17 0zm0 1 12.6 0c-0.158 0.336-0.373 0.631-0.523 0.962h-3.1v1.38c-0.4 0.05-0.7 0.08-1 0.19v-1.57h-8v8h8l0.047-5.61c0.3 0.26 0.6 0.54 1 0.84v4.77h8l0.047-4.09c0.3-0.267 0.654-0.579 1-0.873v14c-0.0019 0.553-0.446 1-1 1h-17c-0.55 0-1-0.4-1-1v-17c0-0.55 0.45-1 1-1zm14.2 0.04 2.8-0.04c0.554-0.00749 1 0.447 1 1v1.37c-0.3-0.04-0.6-0.03-1-0.06v-1.31h-2.3c-0.166-0.346-0.31-0.623-0.486-0.962zm-0.875 0.343c0.512 0.0155 1.05 1.93 1.47 2.21 0.394 0.266 2.46 0.0956 2.59 0.53 0.143 0.458-1.65 1.51-1.81 1.97-0.15 0.43 0.653 2.21 0.25 2.46-0.424 0.267-2.05-1-2.56-0.998-0.487 0.000001-2.05 1.28-2.44 1-0.405-0.292 0.377-2.14 0.219-2.59-0.15-0.43-1.91-1.41-1.75-1.84 0.174-0.448 2.3-0.313 2.72-0.593 0.394-0.266 0.825-2.17 1.31-2.15zm-13.3 9.62v8h8v-8zm9 0v8h8v-8z"/>
+</svg>
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 (file)
index 0000000..5a75498
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m19.61186 12.49993s-2.5481-0.000234-3.9688 0v-10h-9v10c-1.4428-0.000174-3.994 0.000778-4 0l8.5 8.5 8.4688-8.5z" fill="#888"/>
+</svg>
diff --git a/src/pentobi_qml/qml/icons/pentobi-play.svg b/src/pentobi_qml/qml/icons/pentobi-play.svg
new file mode 100644 (file)
index 0000000..e5441ce
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <path fill="#888" d="m2.5 0c-1.35 0-2.5 1.15-2.5 2.5v15c0 1.3 1.15 2.5 2.5 2.5h9.5v2l2.69-2h4.81c1.35 0 2.5-1.15 2.5-2.5v-15c0-1.35-1.2-2.5-2.5-2.5zm1.06 3h14.9c0.2 0 0.5 0.26 0.5 0.56v8.5l-1-0.8v-7.3h-14v11h8v1h-8.44c-0.3 0-0.56-0.3-0.56-0.6v-11.8c0-0.34 0.26-0.6 0.56-0.6zm9.44 6.3l7 5.2-7 5.1zm-10 7.7h9v1h-9zm16-0.1v1.1h-1.7z"/>
+</svg>
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 (file)
index 0000000..a58d536
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m19.469 9.5s-2.5481 0.000234-3.9688 0v10h-9v-10c-1.4428 0.000174-3.994-0.000778-4 0l8.5-8.5 8.4688 8.5z" fill="#888"/>
+</svg>
diff --git a/src/pentobi_qml/qml/icons/pentobi-undo.svg b/src/pentobi_qml/qml/icons/pentobi-undo.svg
new file mode 100644 (file)
index 0000000..1c23026
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <path fill="#888" d="m1 0c-0.603 0-1 0.397-1 1v19c0 0.603 0.397 1 1 1h19c0.603 0 1-0.397 1-1v-19c0-0.603-0.4-1-1-1zm0.6 1h17.7c0.546 0 0.7 0.154 0.7 0.7v17.6c0 0.546-0.154 0.7-0.7 0.7-3.49-0.0171-7.56-0.0022-10.7-0.000002l0.00586-1h1.38v-3.21c0.364 0.0398 0.721 0.118 1.03 0.27l-0.0254 2.94h8v-8h-8v0.193c-0.673-0.313-1.11-0.566-2-0.641v-0.553h1v-8h-8v8h2.28l-1.05 1h-1.23v1.18l-1 0.822 1 0.883v5.12h5.23l1.02 1h-6.65c-0.55 0-0.6-0.2-0.6-0.7v-6.3-11.3c0-0.55 0.05-0.7 0.6-0.7zm9.4 1v8h8v-8zm-3 5v4c3.07 0.258 4.71 2.24 4.84 2.46 1.38 1.52 2.07 3.83 2.16 5.48 0 0-2.15-2.57-3.11-3.09-0.9-0.5-1.8-0.8-3.9-0.8v4l-6-6z"/>
+</svg>
diff --git a/src/pentobi_qml/qml/themes/dark/Theme.qml b/src/pentobi_qml/qml/themes/dark/Theme.qml
new file mode 100644 (file)
index 0000000..edaac53
--- /dev/null
@@ -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 (file)
index 0000000..dedd14c
--- /dev/null
@@ -0,0 +1,156 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="224" width="224" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<g id="a" transform="translate(98)">
+<rect height="14" width="14" fill="#494347"/>
+<path d="m0.5 13.5v-13h13l-0.5 0.5h-12v12z" fill="#3b3639"/>
+<path d="m0.5 13.5h13v-13l-0.5 0.5v12h-12z" fill="#6d686b"/>
+</g>
+<use xlink:href="#a" transform="translate(-98 98)"/>
+<use xlink:href="#a" transform="translate(-98 112)"/>
+<use xlink:href="#a" transform="translate(-84 84)"/>
+<use xlink:href="#a" transform="translate(-84 98)"/>
+<use xlink:href="#a" transform="translate(-84 112)"/>
+<use xlink:href="#a" transform="translate(-84 126)"/>
+<use xlink:href="#a" transform="translate(-70 70)"/>
+<use xlink:href="#a" transform="translate(-70 84)"/>
+<use xlink:href="#a" transform="translate(-70 98)"/>
+<use xlink:href="#a" transform="translate(-70 112)"/>
+<use xlink:href="#a" transform="translate(-70 126)"/>
+<use xlink:href="#a" transform="translate(-70 140)"/>
+<use xlink:href="#a" transform="translate(-56 56)"/>
+<use xlink:href="#a" transform="translate(-56 70)"/>
+<use xlink:href="#a" transform="translate(-56 84)"/>
+<use xlink:href="#a" transform="translate(-56 98)"/>
+<use xlink:href="#a" transform="translate(-56 112)"/>
+<use xlink:href="#a" transform="translate(-56 126)"/>
+<use xlink:href="#a" transform="translate(-56 140)"/>
+<use xlink:href="#a" transform="translate(-56 154)"/>
+<use xlink:href="#a" transform="translate(-42 42)"/>
+<use xlink:href="#a" transform="translate(-42 56)"/>
+<use xlink:href="#a" transform="translate(-42 70)"/>
+<use xlink:href="#a" transform="translate(-42 84)"/>
+<use xlink:href="#a" transform="translate(-42 98)"/>
+<use xlink:href="#a" transform="translate(-42 112)"/>
+<use xlink:href="#a" transform="translate(-42 126)"/>
+<use xlink:href="#a" transform="translate(-42 140)"/>
+<use xlink:href="#a" transform="translate(-42 154)"/>
+<use xlink:href="#a" transform="translate(-42 168)"/>
+<use xlink:href="#a" transform="translate(-28 28)"/>
+<use xlink:href="#a" transform="translate(-28 42)"/>
+<use xlink:href="#a" transform="translate(-28 56)"/>
+<use xlink:href="#a" transform="translate(-28 70)"/>
+<use xlink:href="#a" transform="translate(-28 84)"/>
+<use xlink:href="#a" transform="translate(-28 126)"/>
+<use xlink:href="#a" transform="translate(-28 140)"/>
+<use xlink:href="#a" transform="translate(-28 154)"/>
+<use xlink:href="#a" transform="translate(-28 168)"/>
+<use xlink:href="#a" transform="translate(-28 182)"/>
+<use xlink:href="#a" transform="translate(-14 14)"/>
+<use xlink:href="#a" transform="translate(-14 28)"/>
+<use xlink:href="#a" transform="translate(-14 42)"/>
+<use xlink:href="#a" transform="translate(-14 56)"/>
+<use xlink:href="#a" transform="translate(-14 70)"/>
+<use xlink:href="#a" transform="translate(-14 140)"/>
+<use xlink:href="#a" transform="translate(-14 154)"/>
+<use xlink:href="#a" transform="translate(-14 168)"/>
+<use xlink:href="#a" transform="translate(-14 182)"/>
+<use xlink:href="#a" transform="translate(-14 196)"/>
+<use xlink:href="#a" transform="translate(0 14)"/>
+<use xlink:href="#a" transform="translate(0 28)"/>
+<use xlink:href="#a" transform="translate(0 42)"/>
+<use xlink:href="#a" transform="translate(0 56)"/>
+<use xlink:href="#a" transform="translate(0 154)"/>
+<use xlink:href="#a" transform="translate(0 168)"/>
+<use xlink:href="#a" transform="translate(0 182)"/>
+<use xlink:href="#a" transform="translate(0 196)"/>
+<use xlink:href="#a" transform="translate(0 210)"/>
+<use xlink:href="#a" transform="translate(14)"/>
+<use xlink:href="#a" transform="translate(14 14)"/>
+<use xlink:href="#a" transform="translate(14 28)"/>
+<use xlink:href="#a" transform="translate(14 42)"/>
+<use xlink:href="#a" transform="translate(14 56)"/>
+<use xlink:href="#a" transform="translate(14 154)"/>
+<use xlink:href="#a" transform="translate(14 168)"/>
+<use xlink:href="#a" transform="translate(14 182)"/>
+<use xlink:href="#a" transform="translate(14 196)"/>
+<use xlink:href="#a" transform="translate(14 210)"/>
+<use xlink:href="#a" transform="translate(28 14)"/>
+<use xlink:href="#a" transform="translate(28 28)"/>
+<use xlink:href="#a" transform="translate(28 42)"/>
+<use xlink:href="#a" transform="translate(28 56)"/>
+<use xlink:href="#a" transform="translate(28 70)"/>
+<use xlink:href="#a" transform="translate(28 140)"/>
+<use xlink:href="#a" transform="translate(28 154)"/>
+<use xlink:href="#a" transform="translate(28 168)"/>
+<use xlink:href="#a" transform="translate(28 182)"/>
+<use xlink:href="#a" transform="translate(28 196)"/>
+<use xlink:href="#a" transform="translate(42 28)"/>
+<use xlink:href="#a" transform="translate(42 42)"/>
+<use xlink:href="#a" transform="translate(42 56)"/>
+<use xlink:href="#a" transform="translate(42 70)"/>
+<use xlink:href="#a" transform="translate(42 84)"/>
+<use xlink:href="#a" transform="translate(42 126)"/>
+<use xlink:href="#a" transform="translate(42 140)"/>
+<use xlink:href="#a" transform="translate(42 154)"/>
+<use xlink:href="#a" transform="translate(42 168)"/>
+<use xlink:href="#a" transform="translate(42 182)"/>
+<use xlink:href="#a" transform="translate(56 42)"/>
+<use xlink:href="#a" transform="translate(56 56)"/>
+<use xlink:href="#a" transform="translate(56 70)"/>
+<use xlink:href="#a" transform="translate(56 84)"/>
+<use xlink:href="#a" transform="translate(56 98)"/>
+<use xlink:href="#a" transform="translate(56 112)"/>
+<use xlink:href="#a" transform="translate(56 126)"/>
+<use xlink:href="#a" transform="translate(56 140)"/>
+<use xlink:href="#a" transform="translate(56 154)"/>
+<use xlink:href="#a" transform="translate(56 168)"/>
+<use xlink:href="#a" transform="translate(70 56)"/>
+<use xlink:href="#a" transform="translate(70 70)"/>
+<use xlink:href="#a" transform="translate(70 84)"/>
+<use xlink:href="#a" transform="translate(70 98)"/>
+<use xlink:href="#a" transform="translate(70 112)"/>
+<use xlink:href="#a" transform="translate(70 126)"/>
+<use xlink:href="#a" transform="translate(70 140)"/>
+<use xlink:href="#a" transform="translate(70 154)"/>
+<use xlink:href="#a" transform="translate(84 70)"/>
+<use xlink:href="#a" transform="translate(84 84)"/>
+<use xlink:href="#a" transform="translate(84 98)"/>
+<use xlink:href="#a" transform="translate(84 112)"/>
+<use xlink:href="#a" transform="translate(84 126)"/>
+<use xlink:href="#a" transform="translate(84 140)"/>
+<use xlink:href="#a" transform="translate(98 84)"/>
+<use xlink:href="#a" transform="translate(98 98)"/>
+<use xlink:href="#a" transform="translate(98 112)"/>
+<use xlink:href="#a" transform="translate(98 126)"/>
+<use xlink:href="#a" transform="translate(112,98)"/>
+<use xlink:href="#a" transform="translate(112,112)"/>
+<g id="b" transform="translate(98 70)">
+<rect height="14" width="14" fill="#494347"/>
+<rect height="12" width="12" y="1" x="1" fill="#696267"/>
+<path d="m0.5 13.5v-13h13l-0.5 0.5h-12v12z" fill="#5a5458"/>
+<path d="m0.5 13.5h13v-13l-0.5 0.5v12h-12z" fill="#797276"/>
+</g>
+<use xlink:href="#b" transform="translate(-28,28)"/>
+<use xlink:href="#b" transform="translate(-28,42)"/>
+<use xlink:href="#b" transform="translate(-14,14)"/>
+<use xlink:href="#b" transform="translate(-14,28)"/>
+<use xlink:href="#b" transform="translate(-14,42)"/>
+<use xlink:href="#b" transform="translate(-14,56)"/>
+<use xlink:href="#b" transform="translate(0,14)"/>
+<use xlink:href="#b" transform="translate(0,28)"/>
+<use xlink:href="#b" transform="translate(0,42)"/>
+<use xlink:href="#b" transform="translate(0,56)"/>
+<use xlink:href="#b" transform="translate(0,70)"/>
+<use xlink:href="#b" transform="translate(14)"/>
+<use xlink:href="#b" transform="translate(14,14)"/>
+<use xlink:href="#b" transform="translate(14,28)"/>
+<use xlink:href="#b" transform="translate(14,42)"/>
+<use xlink:href="#b" transform="translate(14,56)"/>
+<use xlink:href="#b" transform="translate(14,70)"/>
+<use xlink:href="#b" transform="translate(28,14)"/>
+<use xlink:href="#b" transform="translate(28,28)"/>
+<use xlink:href="#b" transform="translate(28,42)"/>
+<use xlink:href="#b" transform="translate(28,56)"/>
+<use xlink:href="#b" transform="translate(42,28)"/>
+<use xlink:href="#b" transform="translate(42,42)"/>
+</svg>
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 (file)
index 0000000..b97a7f9
--- /dev/null
@@ -0,0 +1,232 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="280" width="280" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<g id="a" transform="translate(126)">
+<rect height="14" width="14" fill="#494347"/>
+<path d="m0.5 13.5v-13h13l-0.5 0.5h-12v12z" fill="#3b3639"/>
+<path d="m0.5 13.5h13v-13l-0.5 0.5v12h-12z" fill="#6d686b"/>
+</g>
+<use xlink:href="#a" transform="translate(-126,126)"/>
+<use xlink:href="#a" transform="translate(-126,140)"/>
+<use xlink:href="#a" transform="translate(-112,112)"/>
+<use xlink:href="#a" transform="translate(-112,126)"/>
+<use xlink:href="#a" transform="translate(-112,140)"/>
+<use xlink:href="#a" transform="translate(-112,154)"/>
+<use xlink:href="#a" transform="translate(-98 98)"/>
+<use xlink:href="#a" transform="translate(-98 112)"/>
+<use xlink:href="#a" transform="translate(-98 126)"/>
+<use xlink:href="#a" transform="translate(-98 140)"/>
+<use xlink:href="#a" transform="translate(-98 154)"/>
+<use xlink:href="#a" transform="translate(-98 168)"/>
+<use xlink:href="#a" transform="translate(-84 84)"/>
+<use xlink:href="#a" transform="translate(-84 98)"/>
+<use xlink:href="#a" transform="translate(-84 112)"/>
+<use xlink:href="#a" transform="translate(-84 126)"/>
+<use xlink:href="#a" transform="translate(-84 140)"/>
+<use xlink:href="#a" transform="translate(-84 154)"/>
+<use xlink:href="#a" transform="translate(-84 168)"/>
+<use xlink:href="#a" transform="translate(-84 182)"/>
+<use xlink:href="#a" transform="translate(-70 70)"/>
+<use xlink:href="#a" transform="translate(-70 84)"/>
+<use xlink:href="#a" transform="translate(-70 98)"/>
+<use xlink:href="#a" transform="translate(-70 112)"/>
+<use xlink:href="#a" transform="translate(-70 126)"/>
+<use xlink:href="#a" transform="translate(-70 140)"/>
+<use xlink:href="#a" transform="translate(-70 154)"/>
+<use xlink:href="#a" transform="translate(-70 168)"/>
+<use xlink:href="#a" transform="translate(-70 182)"/>
+<use xlink:href="#a" transform="translate(-70 196)"/>
+<use xlink:href="#a" transform="translate(-56 56)"/>
+<use xlink:href="#a" transform="translate(-56 70)"/>
+<use xlink:href="#a" transform="translate(-56 84)"/>
+<use xlink:href="#a" transform="translate(-56 98)"/>
+<use xlink:href="#a" transform="translate(-56 112)"/>
+<use xlink:href="#a" transform="translate(-56 126)"/>
+<use xlink:href="#a" transform="translate(-56 140)"/>
+<use xlink:href="#a" transform="translate(-56 154)"/>
+<use xlink:href="#a" transform="translate(-56 168)"/>
+<use xlink:href="#a" transform="translate(-56 182)"/>
+<use xlink:href="#a" transform="translate(-56 196)"/>
+<use xlink:href="#a" transform="translate(-56 210)"/>
+<use xlink:href="#a" transform="translate(-42 42)"/>
+<use xlink:href="#a" transform="translate(-42 56)"/>
+<use xlink:href="#a" transform="translate(-42 70)"/>
+<use xlink:href="#a" transform="translate(-42 84)"/>
+<use xlink:href="#a" transform="translate(-42 98)"/>
+<use xlink:href="#a" transform="translate(-42 112)"/>
+<use xlink:href="#a" transform="translate(-42 126)"/>
+<use xlink:href="#a" transform="translate(-42 140)"/>
+<use xlink:href="#a" transform="translate(-42 154)"/>
+<use xlink:href="#a" transform="translate(-42 168)"/>
+<use xlink:href="#a" transform="translate(-42 182)"/>
+<use xlink:href="#a" transform="translate(-42 196)"/>
+<use xlink:href="#a" transform="translate(-42 210)"/>
+<use xlink:href="#a" transform="translate(-42 224)"/>
+<use xlink:href="#a" transform="translate(-28 28)"/>
+<use xlink:href="#a" transform="translate(-28 42)"/>
+<use xlink:href="#a" transform="translate(-28 56)"/>
+<use xlink:href="#a" transform="translate(-28 70)"/>
+<use xlink:href="#a" transform="translate(-28 84)"/>
+<use xlink:href="#a" transform="translate(-28 98)"/>
+<use xlink:href="#a" transform="translate(-28 112)"/>
+<use xlink:href="#a" transform="translate(-28 154)"/>
+<use xlink:href="#a" transform="translate(-28 168)"/>
+<use xlink:href="#a" transform="translate(-28 182)"/>
+<use xlink:href="#a" transform="translate(-28 196)"/>
+<use xlink:href="#a" transform="translate(-28 210)"/>
+<use xlink:href="#a" transform="translate(-28 224)"/>
+<use xlink:href="#a" transform="translate(-28 238)"/>
+<use xlink:href="#a" transform="translate(-14 14)"/>
+<use xlink:href="#a" transform="translate(-14 28)"/>
+<use xlink:href="#a" transform="translate(-14 42)"/>
+<use xlink:href="#a" transform="translate(-14 56)"/>
+<use xlink:href="#a" transform="translate(-14 70)"/>
+<use xlink:href="#a" transform="translate(-14 84)"/>
+<use xlink:href="#a" transform="translate(-14 98)"/>
+<use xlink:href="#a" transform="translate(-14 168)"/>
+<use xlink:href="#a" transform="translate(-14 182)"/>
+<use xlink:href="#a" transform="translate(-14 196)"/>
+<use xlink:href="#a" transform="translate(-14 210)"/>
+<use xlink:href="#a" transform="translate(-14 224)"/>
+<use xlink:href="#a" transform="translate(-14 238)"/>
+<use xlink:href="#a" transform="translate(-14 252)"/>
+<use xlink:href="#a" transform="translate(0 14)"/>
+<use xlink:href="#a" transform="translate(0 28)"/>
+<use xlink:href="#a" transform="translate(0 42)"/>
+<use xlink:href="#a" transform="translate(0 56)"/>
+<use xlink:href="#a" transform="translate(0 70)"/>
+<use xlink:href="#a" transform="translate(0 84)"/>
+<use xlink:href="#a" transform="translate(0 182)"/>
+<use xlink:href="#a" transform="translate(0 196)"/>
+<use xlink:href="#a" transform="translate(0 210)"/>
+<use xlink:href="#a" transform="translate(0 224)"/>
+<use xlink:href="#a" transform="translate(0 238)"/>
+<use xlink:href="#a" transform="translate(0 252)"/>
+<use xlink:href="#a" transform="translate(0 266)"/>
+<use xlink:href="#a" transform="translate(14)"/>
+<use xlink:href="#a" transform="translate(14 14)"/>
+<use xlink:href="#a" transform="translate(14 28)"/>
+<use xlink:href="#a" transform="translate(14 42)"/>
+<use xlink:href="#a" transform="translate(14 56)"/>
+<use xlink:href="#a" transform="translate(14 70)"/>
+<use xlink:href="#a" transform="translate(14 84)"/>
+<use xlink:href="#a" transform="translate(14 182)"/>
+<use xlink:href="#a" transform="translate(14 196)"/>
+<use xlink:href="#a" transform="translate(14 210)"/>
+<use xlink:href="#a" transform="translate(14 224)"/>
+<use xlink:href="#a" transform="translate(14 238)"/>
+<use xlink:href="#a" transform="translate(14 252)"/>
+<use xlink:href="#a" transform="translate(14 266)"/>
+<use xlink:href="#a" transform="translate(28 14)"/>
+<use xlink:href="#a" transform="translate(28 28)"/>
+<use xlink:href="#a" transform="translate(28 42)"/>
+<use xlink:href="#a" transform="translate(28 56)"/>
+<use xlink:href="#a" transform="translate(28 70)"/>
+<use xlink:href="#a" transform="translate(28 84)"/>
+<use xlink:href="#a" transform="translate(28 98)"/>
+<use xlink:href="#a" transform="translate(28 168)"/>
+<use xlink:href="#a" transform="translate(28 182)"/>
+<use xlink:href="#a" transform="translate(28 196)"/>
+<use xlink:href="#a" transform="translate(28 210)"/>
+<use xlink:href="#a" transform="translate(28 224)"/>
+<use xlink:href="#a" transform="translate(28 238)"/>
+<use xlink:href="#a" transform="translate(28 252)"/>
+<use xlink:href="#a" transform="translate(42 28)"/>
+<use xlink:href="#a" transform="translate(42 42)"/>
+<use xlink:href="#a" transform="translate(42 56)"/>
+<use xlink:href="#a" transform="translate(42 70)"/>
+<use xlink:href="#a" transform="translate(42 84)"/>
+<use xlink:href="#a" transform="translate(42 98)"/>
+<use xlink:href="#a" transform="translate(42 112)"/>
+<use xlink:href="#a" transform="translate(42 154)"/>
+<use xlink:href="#a" transform="translate(42 168)"/>
+<use xlink:href="#a" transform="translate(42 182)"/>
+<use xlink:href="#a" transform="translate(42 196)"/>
+<use xlink:href="#a" transform="translate(42 210)"/>
+<use xlink:href="#a" transform="translate(42 224)"/>
+<use xlink:href="#a" transform="translate(42 238)"/>
+<use xlink:href="#a" transform="translate(56 42)"/>
+<use xlink:href="#a" transform="translate(56 56)"/>
+<use xlink:href="#a" transform="translate(56 70)"/>
+<use xlink:href="#a" transform="translate(56 84)"/>
+<use xlink:href="#a" transform="translate(56 98)"/>
+<use xlink:href="#a" transform="translate(56 112)"/>
+<use xlink:href="#a" transform="translate(56 126)"/>
+<use xlink:href="#a" transform="translate(56 140)"/>
+<use xlink:href="#a" transform="translate(56 154)"/>
+<use xlink:href="#a" transform="translate(56 168)"/>
+<use xlink:href="#a" transform="translate(56 182)"/>
+<use xlink:href="#a" transform="translate(56 196)"/>
+<use xlink:href="#a" transform="translate(56 210)"/>
+<use xlink:href="#a" transform="translate(56 224)"/>
+<use xlink:href="#a" transform="translate(70 56)"/>
+<use xlink:href="#a" transform="translate(70 70)"/>
+<use xlink:href="#a" transform="translate(70 84)"/>
+<use xlink:href="#a" transform="translate(70 98)"/>
+<use xlink:href="#a" transform="translate(70 112)"/>
+<use xlink:href="#a" transform="translate(70 126)"/>
+<use xlink:href="#a" transform="translate(70 140)"/>
+<use xlink:href="#a" transform="translate(70 154)"/>
+<use xlink:href="#a" transform="translate(70 168)"/>
+<use xlink:href="#a" transform="translate(70 182)"/>
+<use xlink:href="#a" transform="translate(70 196)"/>
+<use xlink:href="#a" transform="translate(70 210)"/>
+<use xlink:href="#a" transform="translate(84 70)"/>
+<use xlink:href="#a" transform="translate(84 84)"/>
+<use xlink:href="#a" transform="translate(84 98)"/>
+<use xlink:href="#a" transform="translate(84 112)"/>
+<use xlink:href="#a" transform="translate(84 126)"/>
+<use xlink:href="#a" transform="translate(84 140)"/>
+<use xlink:href="#a" transform="translate(84 154)"/>
+<use xlink:href="#a" transform="translate(84 168)"/>
+<use xlink:href="#a" transform="translate(84 182)"/>
+<use xlink:href="#a" transform="translate(84 196)"/>
+<use xlink:href="#a" transform="translate(98 84)"/>
+<use xlink:href="#a" transform="translate(98 98)"/>
+<use xlink:href="#a" transform="translate(98 112)"/>
+<use xlink:href="#a" transform="translate(98 126)"/>
+<use xlink:href="#a" transform="translate(98 140)"/>
+<use xlink:href="#a" transform="translate(98 154)"/>
+<use xlink:href="#a" transform="translate(98 168)"/>
+<use xlink:href="#a" transform="translate(98 182)"/>
+<use xlink:href="#a" transform="translate(112,98)"/>
+<use xlink:href="#a" transform="translate(112,112)"/>
+<use xlink:href="#a" transform="translate(112,126)"/>
+<use xlink:href="#a" transform="translate(112,140)"/>
+<use xlink:href="#a" transform="translate(112,154)"/>
+<use xlink:href="#a" transform="translate(112,168)"/>
+<use xlink:href="#a" transform="translate(126,112)"/>
+<use xlink:href="#a" transform="translate(126,126)"/>
+<use xlink:href="#a" transform="translate(126,140)"/>
+<use xlink:href="#a" transform="translate(126,154)"/>
+<use xlink:href="#a" transform="translate(140,126)"/>
+<use xlink:href="#a" transform="translate(140,140)"/>
+<g id="b" transform="translate(126 98)">
+<rect height="14" width="14" fill="#494347"/>
+<rect height="12" width="12" y="1" x="1" fill="#696267"/>
+<path d="m0.5 13.5v-13h13l-0.5 0.5h-12v12z" fill="#5a5458"/>
+<path d="m0.5 13.5h13v-13l-0.5 0.5v12h-12z" fill="#797276"/>
+</g>
+<use xlink:href="#b" transform="translate(-28,28)"/>
+<use xlink:href="#b" transform="translate(-28,42)"/>
+<use xlink:href="#b" transform="translate(-14,14)"/>
+<use xlink:href="#b" transform="translate(-14,28)"/>
+<use xlink:href="#b" transform="translate(-14,42)"/>
+<use xlink:href="#b" transform="translate(-14,56)"/>
+<use xlink:href="#b" transform="translate(0,14)"/>
+<use xlink:href="#b" transform="translate(0,28)"/>
+<use xlink:href="#b" transform="translate(0,42)"/>
+<use xlink:href="#b" transform="translate(0,56)"/>
+<use xlink:href="#b" transform="translate(0,70)"/>
+<use xlink:href="#b" transform="translate(14)"/>
+<use xlink:href="#b" transform="translate(14,14)"/>
+<use xlink:href="#b" transform="translate(14,28)"/>
+<use xlink:href="#b" transform="translate(14,42)"/>
+<use xlink:href="#b" transform="translate(14,56)"/>
+<use xlink:href="#b" transform="translate(14,70)"/>
+<use xlink:href="#b" transform="translate(28,14)"/>
+<use xlink:href="#b" transform="translate(28,28)"/>
+<use xlink:href="#b" transform="translate(28,42)"/>
+<use xlink:href="#b" transform="translate(28,56)"/>
+<use xlink:href="#b" transform="translate(42,28)"/>
+<use xlink:href="#b" transform="translate(42,42)"/>
+</svg>
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 (file)
index 0000000..17b22f3
--- /dev/null
@@ -0,0 +1,300 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="280" width="280" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<g id="a" transform="translate(98)">
+<rect height="14" width="14" fill="#494347"/>
+<path d="m0.5 13.5v-13h13l-0.5 0.5h-12v12z" fill="#3b3639"/>
+<path d="m0.5 13.5h13v-13l-0.5 0.5v12h-12z" fill="#6d686b"/>
+</g>
+<use xlink:href="#a" transform="translate(-98 98)"/>
+<use xlink:href="#a" transform="translate(-98,112)"/>
+<use xlink:href="#a" transform="translate(-98,126)"/>
+<use xlink:href="#a" transform="translate(-98,140)"/>
+<use xlink:href="#a" transform="translate(-98,154)"/>
+<use xlink:href="#a" transform="translate(-98,168)"/>
+<use xlink:href="#a" transform="translate(-84 84)"/>
+<use xlink:href="#a" transform="translate(-84 98)"/>
+<use xlink:href="#a" transform="translate(-84,112)"/>
+<use xlink:href="#a" transform="translate(-84,126)"/>
+<use xlink:href="#a" transform="translate(-84,140)"/>
+<use xlink:href="#a" transform="translate(-84,154)"/>
+<use xlink:href="#a" transform="translate(-84,168)"/>
+<use xlink:href="#a" transform="translate(-84,182)"/>
+<use xlink:href="#a" transform="translate(-70 70)"/>
+<use xlink:href="#a" transform="translate(-70 84)"/>
+<use xlink:href="#a" transform="translate(-70 98)"/>
+<use xlink:href="#a" transform="translate(-70,112)"/>
+<use xlink:href="#a" transform="translate(-70,126)"/>
+<use xlink:href="#a" transform="translate(-70,140)"/>
+<use xlink:href="#a" transform="translate(-70,154)"/>
+<use xlink:href="#a" transform="translate(-70,168)"/>
+<use xlink:href="#a" transform="translate(-70,182)"/>
+<use xlink:href="#a" transform="translate(-70,196)"/>
+<use xlink:href="#a" transform="translate(-56,56)"/>
+<use xlink:href="#a" transform="translate(-56 70)"/>
+<use xlink:href="#a" transform="translate(-56 84)"/>
+<use xlink:href="#a" transform="translate(-56 98)"/>
+<use xlink:href="#a" transform="translate(-56,112)"/>
+<use xlink:href="#a" transform="translate(-56,126)"/>
+<use xlink:href="#a" transform="translate(-56,140)"/>
+<use xlink:href="#a" transform="translate(-56,154)"/>
+<use xlink:href="#a" transform="translate(-56,168)"/>
+<use xlink:href="#a" transform="translate(-56,182)"/>
+<use xlink:href="#a" transform="translate(-56,196)"/>
+<use xlink:href="#a" transform="translate(-56,210)"/>
+<use xlink:href="#a" transform="translate(-42 42)"/>
+<use xlink:href="#a" transform="translate(-42 56)"/>
+<use xlink:href="#a" transform="translate(-42 70)"/>
+<use xlink:href="#a" transform="translate(-42 84)"/>
+<use xlink:href="#a" transform="translate(-42 98)"/>
+<use xlink:href="#a" transform="translate(-42 112)"/>
+<use xlink:href="#a" transform="translate(-42 126)"/>
+<use xlink:href="#a" transform="translate(-42 140)"/>
+<use xlink:href="#a" transform="translate(-42 154)"/>
+<use xlink:href="#a" transform="translate(-42 168)"/>
+<use xlink:href="#a" transform="translate(-42 182)"/>
+<use xlink:href="#a" transform="translate(-42 196)"/>
+<use xlink:href="#a" transform="translate(-42 210)"/>
+<use xlink:href="#a" transform="translate(-42 224)"/>
+<use xlink:href="#a" transform="translate(-28 28)"/>
+<use xlink:href="#a" transform="translate(-28 42)"/>
+<use xlink:href="#a" transform="translate(-28 56)"/>
+<use xlink:href="#a" transform="translate(-28 70)"/>
+<use xlink:href="#a" transform="translate(-28 84)"/>
+<use xlink:href="#a" transform="translate(-28 98)"/>
+<use xlink:href="#a" transform="translate(-28 112)"/>
+<use xlink:href="#a" transform="translate(-28 126)"/>
+<use xlink:href="#a" transform="translate(-28 140)"/>
+<use xlink:href="#a" transform="translate(-28 154)"/>
+<use xlink:href="#a" transform="translate(-28 168)"/>
+<use xlink:href="#a" transform="translate(-28 182)"/>
+<use xlink:href="#a" transform="translate(-28 196)"/>
+<use xlink:href="#a" transform="translate(-28 210)"/>
+<use xlink:href="#a" transform="translate(-28 224)"/>
+<use xlink:href="#a" transform="translate(-28 238)"/>
+<use xlink:href="#a" transform="translate(-14 14)"/>
+<use xlink:href="#a" transform="translate(-14 28)"/>
+<use xlink:href="#a" transform="translate(-14 42)"/>
+<use xlink:href="#a" transform="translate(-14 56)"/>
+<use xlink:href="#a" transform="translate(-14 70)"/>
+<use xlink:href="#a" transform="translate(-14 84)"/>
+<use xlink:href="#a" transform="translate(-14 98)"/>
+<use xlink:href="#a" transform="translate(-14 112)"/>
+<use xlink:href="#a" transform="translate(-14 126)"/>
+<use xlink:href="#a" transform="translate(-14 140)"/>
+<use xlink:href="#a" transform="translate(-14 154)"/>
+<use xlink:href="#a" transform="translate(-14 168)"/>
+<use xlink:href="#a" transform="translate(-14 182)"/>
+<use xlink:href="#a" transform="translate(-14 196)"/>
+<use xlink:href="#a" transform="translate(-14 210)"/>
+<use xlink:href="#a" transform="translate(-14 224)"/>
+<use xlink:href="#a" transform="translate(-14 238)"/>
+<use xlink:href="#a" transform="translate(-14 252)"/>
+<use xlink:href="#a" transform="translate(0 14)"/>
+<use xlink:href="#a" transform="translate(0 28)"/>
+<use xlink:href="#a" transform="translate(0 42)"/>
+<use xlink:href="#a" transform="translate(0 56)"/>
+<use xlink:href="#a" transform="translate(0 70)"/>
+<use xlink:href="#a" transform="translate(0 84)"/>
+<use xlink:href="#a" transform="translate(0 98)"/>
+<use xlink:href="#a" transform="translate(0 112)"/>
+<use xlink:href="#a" transform="translate(0 154)"/>
+<use xlink:href="#a" transform="translate(0 168)"/>
+<use xlink:href="#a" transform="translate(0 182)"/>
+<use xlink:href="#a" transform="translate(0 196)"/>
+<use xlink:href="#a" transform="translate(0 210)"/>
+<use xlink:href="#a" transform="translate(0 224)"/>
+<use xlink:href="#a" transform="translate(0 238)"/>
+<use xlink:href="#a" transform="translate(0 252)"/>
+<use xlink:href="#a" transform="translate(0 266)"/>
+<use xlink:href="#a" transform="translate(14)"/>
+<use xlink:href="#a" transform="translate(14 14)"/>
+<use xlink:href="#a" transform="translate(14 28)"/>
+<use xlink:href="#a" transform="translate(14 42)"/>
+<use xlink:href="#a" transform="translate(14 56)"/>
+<use xlink:href="#a" transform="translate(14 70)"/>
+<use xlink:href="#a" transform="translate(14 84)"/>
+<use xlink:href="#a" transform="translate(14 98)"/>
+<use xlink:href="#a" transform="translate(14 168)"/>
+<use xlink:href="#a" transform="translate(14 182)"/>
+<use xlink:href="#a" transform="translate(14 196)"/>
+<use xlink:href="#a" transform="translate(14 210)"/>
+<use xlink:href="#a" transform="translate(14 224)"/>
+<use xlink:href="#a" transform="translate(14 238)"/>
+<use xlink:href="#a" transform="translate(14 252)"/>
+<use xlink:href="#a" transform="translate(14 266)"/>
+<use xlink:href="#a" transform="translate(28)"/>
+<use xlink:href="#a" transform="translate(28 14)"/>
+<use xlink:href="#a" transform="translate(28 28)"/>
+<use xlink:href="#a" transform="translate(28 42)"/>
+<use xlink:href="#a" transform="translate(28 56)"/>
+<use xlink:href="#a" transform="translate(28 70)"/>
+<use xlink:href="#a" transform="translate(28 84)"/>
+<use xlink:href="#a" transform="translate(28 182)"/>
+<use xlink:href="#a" transform="translate(28 196)"/>
+<use xlink:href="#a" transform="translate(28 210)"/>
+<use xlink:href="#a" transform="translate(28 224)"/>
+<use xlink:href="#a" transform="translate(28 238)"/>
+<use xlink:href="#a" transform="translate(28 252)"/>
+<use xlink:href="#a" transform="translate(28 266)"/>
+<use xlink:href="#a" transform="translate(42)"/>
+<use xlink:href="#a" transform="translate(42 14)"/>
+<use xlink:href="#a" transform="translate(42 28)"/>
+<use xlink:href="#a" transform="translate(42 42)"/>
+<use xlink:href="#a" transform="translate(42 56)"/>
+<use xlink:href="#a" transform="translate(42 70)"/>
+<use xlink:href="#a" transform="translate(42 84)"/>
+<use xlink:href="#a" transform="translate(42 182)"/>
+<use xlink:href="#a" transform="translate(42 196)"/>
+<use xlink:href="#a" transform="translate(42 210)"/>
+<use xlink:href="#a" transform="translate(42 224)"/>
+<use xlink:href="#a" transform="translate(42 238)"/>
+<use xlink:href="#a" transform="translate(42 252)"/>
+<use xlink:href="#a" transform="translate(42 266)"/>
+<use xlink:href="#a" transform="translate(56)"/>
+<use xlink:href="#a" transform="translate(56 14)"/>
+<use xlink:href="#a" transform="translate(56 28)"/>
+<use xlink:href="#a" transform="translate(56 42)"/>
+<use xlink:href="#a" transform="translate(56 56)"/>
+<use xlink:href="#a" transform="translate(56 70)"/>
+<use xlink:href="#a" transform="translate(56 84)"/>
+<use xlink:href="#a" transform="translate(56 98)"/>
+<use xlink:href="#a" transform="translate(56 168)"/>
+<use xlink:href="#a" transform="translate(56 182)"/>
+<use xlink:href="#a" transform="translate(56 196)"/>
+<use xlink:href="#a" transform="translate(56 210)"/>
+<use xlink:href="#a" transform="translate(56 224)"/>
+<use xlink:href="#a" transform="translate(56 238)"/>
+<use xlink:href="#a" transform="translate(56 252)"/>
+<use xlink:href="#a" transform="translate(56 266)"/>
+<use xlink:href="#a" transform="translate(70)"/>
+<use xlink:href="#a" transform="translate(70 14)"/>
+<use xlink:href="#a" transform="translate(70 28)"/>
+<use xlink:href="#a" transform="translate(70 42)"/>
+<use xlink:href="#a" transform="translate(70 56)"/>
+<use xlink:href="#a" transform="translate(70 70)"/>
+<use xlink:href="#a" transform="translate(70 84)"/>
+<use xlink:href="#a" transform="translate(70 98)"/>
+<use xlink:href="#a" transform="translate(70 112)"/>
+<use xlink:href="#a" transform="translate(70 154)"/>
+<use xlink:href="#a" transform="translate(70 168)"/>
+<use xlink:href="#a" transform="translate(70 182)"/>
+<use xlink:href="#a" transform="translate(70 196)"/>
+<use xlink:href="#a" transform="translate(70 210)"/>
+<use xlink:href="#a" transform="translate(70 224)"/>
+<use xlink:href="#a" transform="translate(70 238)"/>
+<use xlink:href="#a" transform="translate(70 252)"/>
+<use xlink:href="#a" transform="translate(70 266)"/>
+<use xlink:href="#a" transform="translate(84 14)"/>
+<use xlink:href="#a" transform="translate(84 28)"/>
+<use xlink:href="#a" transform="translate(84 42)"/>
+<use xlink:href="#a" transform="translate(84 56)"/>
+<use xlink:href="#a" transform="translate(84 70)"/>
+<use xlink:href="#a" transform="translate(84 84)"/>
+<use xlink:href="#a" transform="translate(84 98)"/>
+<use xlink:href="#a" transform="translate(84 112)"/>
+<use xlink:href="#a" transform="translate(84 126)"/>
+<use xlink:href="#a" transform="translate(84 140)"/>
+<use xlink:href="#a" transform="translate(84 154)"/>
+<use xlink:href="#a" transform="translate(84 168)"/>
+<use xlink:href="#a" transform="translate(84 182)"/>
+<use xlink:href="#a" transform="translate(84 196)"/>
+<use xlink:href="#a" transform="translate(84 210)"/>
+<use xlink:href="#a" transform="translate(84 224)"/>
+<use xlink:href="#a" transform="translate(84 238)"/>
+<use xlink:href="#a" transform="translate(84 252)"/>
+<use xlink:href="#a" transform="translate(98 28)"/>
+<use xlink:href="#a" transform="translate(98 42)"/>
+<use xlink:href="#a" transform="translate(98 56)"/>
+<use xlink:href="#a" transform="translate(98 70)"/>
+<use xlink:href="#a" transform="translate(98 84)"/>
+<use xlink:href="#a" transform="translate(98 98)"/>
+<use xlink:href="#a" transform="translate(98 112)"/>
+<use xlink:href="#a" transform="translate(98 126)"/>
+<use xlink:href="#a" transform="translate(98 140)"/>
+<use xlink:href="#a" transform="translate(98 154)"/>
+<use xlink:href="#a" transform="translate(98 168)"/>
+<use xlink:href="#a" transform="translate(98 182)"/>
+<use xlink:href="#a" transform="translate(98 196)"/>
+<use xlink:href="#a" transform="translate(98 210)"/>
+<use xlink:href="#a" transform="translate(98 224)"/>
+<use xlink:href="#a" transform="translate(98 238)"/>
+<use xlink:href="#a" transform="translate(112 42)"/>
+<use xlink:href="#a" transform="translate(112 56)"/>
+<use xlink:href="#a" transform="translate(112 70)"/>
+<use xlink:href="#a" transform="translate(112 84)"/>
+<use xlink:href="#a" transform="translate(112 98)"/>
+<use xlink:href="#a" transform="translate(112 112)"/>
+<use xlink:href="#a" transform="translate(112 126)"/>
+<use xlink:href="#a" transform="translate(112 140)"/>
+<use xlink:href="#a" transform="translate(112 154)"/>
+<use xlink:href="#a" transform="translate(112 168)"/>
+<use xlink:href="#a" transform="translate(112 182)"/>
+<use xlink:href="#a" transform="translate(112 196)"/>
+<use xlink:href="#a" transform="translate(112 210)"/>
+<use xlink:href="#a" transform="translate(112 224)"/>
+<use xlink:href="#a" transform="translate(126 56)"/>
+<use xlink:href="#a" transform="translate(126 70)"/>
+<use xlink:href="#a" transform="translate(126 84)"/>
+<use xlink:href="#a" transform="translate(126 98)"/>
+<use xlink:href="#a" transform="translate(126 112)"/>
+<use xlink:href="#a" transform="translate(126 126)"/>
+<use xlink:href="#a" transform="translate(126 140)"/>
+<use xlink:href="#a" transform="translate(126 154)"/>
+<use xlink:href="#a" transform="translate(126 168)"/>
+<use xlink:href="#a" transform="translate(126 182)"/>
+<use xlink:href="#a" transform="translate(126 196)"/>
+<use xlink:href="#a" transform="translate(126 210)"/>
+<use xlink:href="#a" transform="translate(140 70)"/>
+<use xlink:href="#a" transform="translate(140 84)"/>
+<use xlink:href="#a" transform="translate(140 98)"/>
+<use xlink:href="#a" transform="translate(140 112)"/>
+<use xlink:href="#a" transform="translate(140 126)"/>
+<use xlink:href="#a" transform="translate(140 140)"/>
+<use xlink:href="#a" transform="translate(140 154)"/>
+<use xlink:href="#a" transform="translate(140 168)"/>
+<use xlink:href="#a" transform="translate(140 182)"/>
+<use xlink:href="#a" transform="translate(140 196)"/>
+<use xlink:href="#a" transform="translate(154 84)"/>
+<use xlink:href="#a" transform="translate(154 98)"/>
+<use xlink:href="#a" transform="translate(154 112)"/>
+<use xlink:href="#a" transform="translate(154 126)"/>
+<use xlink:href="#a" transform="translate(154 140)"/>
+<use xlink:href="#a" transform="translate(154 154)"/>
+<use xlink:href="#a" transform="translate(154 168)"/>
+<use xlink:href="#a" transform="translate(154 182)"/>
+<use xlink:href="#a" transform="translate(168 98)"/>
+<use xlink:href="#a" transform="translate(168 112)"/>
+<use xlink:href="#a" transform="translate(168 126)"/>
+<use xlink:href="#a" transform="translate(168 140)"/>
+<use xlink:href="#a" transform="translate(168 154)"/>
+<use xlink:href="#a" transform="translate(168 168)"/>
+<g id="b" transform="translate(126 98)">
+<rect height="14" width="14" fill="#494347"/>
+<rect height="12" width="12" y="1" x="1" fill="#696267"/>
+<path d="m0.5 13.5v-13h13l-0.5 0.5h-12v12z" fill="#5a5458"/>
+<path d="m0.5 13.5h13v-13l-0.5 0.5v12h-12z" fill="#797276"/>
+</g>
+<use xlink:href="#b" transform="translate(-28,28)"/>
+<use xlink:href="#b" transform="translate(-28,42)"/>
+<use xlink:href="#b" transform="translate(-14,14)"/>
+<use xlink:href="#b" transform="translate(-14,28)"/>
+<use xlink:href="#b" transform="translate(-14,42)"/>
+<use xlink:href="#b" transform="translate(-14,56)"/>
+<use xlink:href="#b" transform="translate(0,14)"/>
+<use xlink:href="#b" transform="translate(0,28)"/>
+<use xlink:href="#b" transform="translate(0,42)"/>
+<use xlink:href="#b" transform="translate(0,56)"/>
+<use xlink:href="#b" transform="translate(0,70)"/>
+<use xlink:href="#b" transform="translate(14)"/>
+<use xlink:href="#b" transform="translate(14,14)"/>
+<use xlink:href="#b" transform="translate(14,28)"/>
+<use xlink:href="#b" transform="translate(14,42)"/>
+<use xlink:href="#b" transform="translate(14,56)"/>
+<use xlink:href="#b" transform="translate(14,70)"/>
+<use xlink:href="#b" transform="translate(28,14)"/>
+<use xlink:href="#b" transform="translate(28,28)"/>
+<use xlink:href="#b" transform="translate(28,42)"/>
+<use xlink:href="#b" transform="translate(28,56)"/>
+<use xlink:href="#b" transform="translate(42,28)"/>
+<use xlink:href="#b" transform="translate(42,42)"/>
+</svg>
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 (file)
index 0000000..a216929
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="14" width="14" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect width="14" height="14" fill="#494347"/>
+<path d="m14 0h-14v14l0.7-0.7v-12.6h12.6z" fill="#3b3639"/>
+<path d="m14 0-0.7 0.7v12.6h-12.6l-0.7 0.7h14z" fill="#6d686b"/>
+</svg>
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 (file)
index 0000000..6be4d0a
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="28" width="28" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect width="28" height="28" fill="#494347"/>
+<g id="a">
+<path d="m0 28v-21h7l-1 1h-5v19z" fill="#3b3639"/>
+<path d="m0 28 1-1h5v-19l1-1v21z" fill="#6d686b"/>
+</g>
+<use xlink:href="#a" transform="matrix(0,1,1,0,0,0)"/>
+</svg>
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 (file)
index 0000000..0964fde
--- /dev/null
@@ -0,0 +1,394 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="320" width="320" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m0 160 80-160h160l80 160-80 160h-160z" fill="#494347"/>
+<g id="c">
+<path d="m70 20 1.732-1.155l8.268-16.845v-2z" fill="#3B3639"/>
+<path d="m70 20h20l-10-20v2l8.27 16.845h-16.538z" fill="#6D686B"/>
+</g>
+<g id="d">
+<path d="m100 0-1.732 1.155l-8.268 16.845v2z" fill="#6D686B"/>
+<path d="m100 0h-20l10 20v-2l-8.27-16.845h16.538z" fill="#3B3639"/>
+</g>
+<use xlink:href="#d" x="-80" y="160"/>
+<use xlink:href="#d" x="-70" y="180"/>
+<use xlink:href="#d" x="-60" y="120"/>
+<use xlink:href="#d" x="-70" y="140"/>
+<use xlink:href="#d" x="-60" y="160"/>
+<use xlink:href="#d" x="-50" y="180"/>
+<use xlink:href="#d" x="-60" y="200"/>
+<use xlink:href="#d" x="-50" y="220"/>
+<use xlink:href="#d" x="-40" y="80"/>
+<use xlink:href="#d" x="-50" y="100"/>
+<use xlink:href="#d" x="-40" y="120"/>
+<use xlink:href="#d" x="-50" y="140"/>
+<use xlink:href="#d" x="-40" y="160"/>
+<use xlink:href="#d" x="-30" y="180"/>
+<use xlink:href="#d" x="-40" y="200"/>
+<use xlink:href="#d" x="-30" y="220"/>
+<use xlink:href="#d" x="-40" y="240"/>
+<use xlink:href="#d" x="-30" y="260"/>
+<use xlink:href="#d" x="-20" y="40"/>
+<use xlink:href="#d" x="-30" y="60"/>
+<use xlink:href="#d" x="-20" y="80"/>
+<use xlink:href="#d" x="-30" y="100"/>
+<use xlink:href="#d" x="-20" y="120"/>
+<use xlink:href="#d" x="-30" y="140"/>
+<use xlink:href="#d" x="-20" y="160"/>
+<use xlink:href="#d" x="-10" y="180"/>
+<use xlink:href="#d" x="-20" y="200"/>
+<use xlink:href="#d" x="-10" y="220"/>
+<use xlink:href="#d" x="-20" y="240"/>
+<use xlink:href="#d" x="-10" y="260"/>
+<use xlink:href="#d" x="-20" y="280"/>
+<use xlink:href="#d" x="-10" y="300"/>
+<use xlink:href="#d" x="-10" y="20"/>
+<use xlink:href="#d" y="40"/>
+<use xlink:href="#d" x="-10" y="60"/>
+<use xlink:href="#d" y="80"/>
+<use xlink:href="#d" x="-10" y="100"/>
+<use xlink:href="#d" y="120"/>
+<use xlink:href="#d" x="-10" y="140"/>
+<use xlink:href="#d" y="160"/>
+<use xlink:href="#d" x="10" y="180"/>
+<use xlink:href="#d" y="200"/>
+<use xlink:href="#d" x="10" y="220"/>
+<use xlink:href="#d" y="240"/>
+<use xlink:href="#d" x="10" y="260"/>
+<use xlink:href="#d" y="280"/>
+<use xlink:href="#d" x="10" y="300"/>
+<use xlink:href="#d" x="20"/>
+<use xlink:href="#d" x="10" y="20"/>
+<use xlink:href="#d" x="20" y="40"/>
+<use xlink:href="#d" x="10" y="60"/>
+<use xlink:href="#d" x="20" y="80"/>
+<use xlink:href="#d" x="10" y="100"/>
+<use xlink:href="#d" x="20" y="120"/>
+<use xlink:href="#d" x="10" y="140"/>
+<use xlink:href="#d" x="20" y="160"/>
+<use xlink:href="#d" x="30" y="180"/>
+<use xlink:href="#d" x="20" y="200"/>
+<use xlink:href="#d" x="30" y="220"/>
+<use xlink:href="#d" x="20" y="240"/>
+<use xlink:href="#d" x="30" y="260"/>
+<use xlink:href="#d" x="20" y="280"/>
+<use xlink:href="#d" x="30" y="300"/>
+<use xlink:href="#d" x="40"/>
+<use xlink:href="#d" x="30" y="20"/>
+<use xlink:href="#d" x="40" y="40"/>
+<use xlink:href="#d" x="30" y="60"/>
+<use xlink:href="#d" x="40" y="80"/>
+<use xlink:href="#d" x="30" y="100"/>
+<use xlink:href="#d" x="40" y="120"/>
+<use xlink:href="#d" x="30" y="140"/>
+<use xlink:href="#d" x="40" y="160"/>
+<use xlink:href="#d" x="50" y="180"/>
+<use xlink:href="#d" x="40" y="200"/>
+<use xlink:href="#d" x="50" y="220"/>
+<use xlink:href="#d" x="40" y="240"/>
+<use xlink:href="#d" x="50" y="260"/>
+<use xlink:href="#d" x="40" y="280"/>
+<use xlink:href="#d" x="50" y="300"/>
+<use xlink:href="#d" x="60"/>
+<use xlink:href="#d" x="50" y="20"/>
+<use xlink:href="#d" x="60" y="40"/>
+<use xlink:href="#d" x="50" y="60"/>
+<use xlink:href="#d" x="60" y="80"/>
+<use xlink:href="#d" x="50" y="100"/>
+<use xlink:href="#d" x="60" y="120"/>
+<use xlink:href="#d" x="50" y="140"/>
+<use xlink:href="#d" x="60" y="160"/>
+<use xlink:href="#d" x="70" y="180"/>
+<use xlink:href="#d" x="60" y="200"/>
+<use xlink:href="#d" x="70" y="220"/>
+<use xlink:href="#d" x="60" y="240"/>
+<use xlink:href="#d" x="70" y="260"/>
+<use xlink:href="#d" x="60" y="280"/>
+<use xlink:href="#d" x="70" y="300"/>
+<use xlink:href="#d" x="80"/>
+<use xlink:href="#d" x="70" y="20"/>
+<use xlink:href="#d" x="80" y="40"/>
+<use xlink:href="#d" x="70" y="60"/>
+<use xlink:href="#d" x="80" y="80"/>
+<use xlink:href="#d" x="70" y="100"/>
+<use xlink:href="#d" x="80" y="120"/>
+<use xlink:href="#d" x="70" y="140"/>
+<use xlink:href="#d" x="80" y="160"/>
+<use xlink:href="#d" x="90" y="180"/>
+<use xlink:href="#d" x="80" y="200"/>
+<use xlink:href="#d" x="90" y="220"/>
+<use xlink:href="#d" x="80" y="240"/>
+<use xlink:href="#d" x="90" y="260"/>
+<use xlink:href="#d" x="80" y="280"/>
+<use xlink:href="#d" x="90" y="300"/>
+<use xlink:href="#d" x="100"/>
+<use xlink:href="#d" x="90" y="20"/>
+<use xlink:href="#d" x="100" y="40"/>
+<use xlink:href="#d" x="90" y="60"/>
+<use xlink:href="#d" x="100" y="80"/>
+<use xlink:href="#d" x="90" y="100"/>
+<use xlink:href="#d" x="100" y="120"/>
+<use xlink:href="#d" x="90" y="140"/>
+<use xlink:href="#d" x="100" y="160"/>
+<use xlink:href="#d" x="110" y="180"/>
+<use xlink:href="#d" x="100" y="200"/>
+<use xlink:href="#d" x="110" y="220"/>
+<use xlink:href="#d" x="100" y="240"/>
+<use xlink:href="#d" x="110" y="260"/>
+<use xlink:href="#d" x="100" y="280"/>
+<use xlink:href="#d" x="110" y="300"/>
+<use xlink:href="#d" x="120"/>
+<use xlink:href="#d" x="110" y="20"/>
+<use xlink:href="#d" x="120" y="40"/>
+<use xlink:href="#d" x="110" y="60"/>
+<use xlink:href="#d" x="120" y="80"/>
+<use xlink:href="#d" x="110" y="100"/>
+<use xlink:href="#d" x="120" y="120"/>
+<use xlink:href="#d" x="110" y="140"/>
+<use xlink:href="#d" x="120" y="160"/>
+<use xlink:href="#d" x="130" y="180"/>
+<use xlink:href="#d" x="120" y="200"/>
+<use xlink:href="#d" x="130" y="220"/>
+<use xlink:href="#d" x="120" y="240"/>
+<use xlink:href="#d" x="130" y="260"/>
+<use xlink:href="#d" x="120" y="280"/>
+<use xlink:href="#d" x="130" y="300"/>
+<use xlink:href="#d" x="140"/>
+<use xlink:href="#d" x="130" y="20"/>
+<use xlink:href="#d" x="140" y="40"/>
+<use xlink:href="#d" x="130" y="60"/>
+<use xlink:href="#d" x="140" y="80"/>
+<use xlink:href="#d" x="130" y="100"/>
+<use xlink:href="#d" x="140" y="120"/>
+<use xlink:href="#d" x="130" y="140"/>
+<use xlink:href="#d" x="140" y="160"/>
+<use xlink:href="#d" x="150" y="180"/>
+<use xlink:href="#d" x="140" y="200"/>
+<use xlink:href="#d" x="150" y="220"/>
+<use xlink:href="#d" x="140" y="240"/>
+<use xlink:href="#d" x="150" y="260"/>
+<use xlink:href="#d" x="140" y="280"/>
+<use xlink:href="#d" x="150" y="300"/>
+<use xlink:href="#d" x="150" y="20"/>
+<use xlink:href="#d" x="160" y="40"/>
+<use xlink:href="#d" x="150" y="60"/>
+<use xlink:href="#d" x="160" y="80"/>
+<use xlink:href="#d" x="150" y="100"/>
+<use xlink:href="#d" x="160" y="120"/>
+<use xlink:href="#d" x="150" y="140"/>
+<use xlink:href="#d" x="160" y="160"/>
+<use xlink:href="#d" x="170" y="180"/>
+<use xlink:href="#d" x="160" y="200"/>
+<use xlink:href="#d" x="170" y="220"/>
+<use xlink:href="#d" x="160" y="240"/>
+<use xlink:href="#d" x="170" y="260"/>
+<use xlink:href="#d" x="160" y="280"/>
+<use xlink:href="#d" x="170" y="60"/>
+<use xlink:href="#d" x="180" y="80"/>
+<use xlink:href="#d" x="170" y="100"/>
+<use xlink:href="#d" x="180" y="120"/>
+<use xlink:href="#d" x="170" y="140"/>
+<use xlink:href="#d" x="180" y="160"/>
+<use xlink:href="#d" x="190" y="180"/>
+<use xlink:href="#d" x="180" y="200"/>
+<use xlink:href="#d" x="190" y="220"/>
+<use xlink:href="#d" x="180" y="240"/>
+<use xlink:href="#d" x="190" y="100"/>
+<use xlink:href="#d" x="200" y="120"/>
+<use xlink:href="#d" x="190" y="140"/>
+<use xlink:href="#d" x="200" y="160"/>
+<use xlink:href="#d" x="210" y="180"/>
+<use xlink:href="#d" x="200" y="200"/>
+<use xlink:href="#d" x="210" y="140"/>
+<use xlink:href="#d" x="220" y="160"/>
+<use xlink:href="#c" x="-60" y="120"/>
+<use xlink:href="#c" x="-70" y="140"/>
+<use xlink:href="#c" x="-60" y="160"/>
+<use xlink:href="#c" x="-50" y="180"/>
+<use xlink:href="#c" x="-40" y="80"/>
+<use xlink:href="#c" x="-50" y="100"/>
+<use xlink:href="#c" x="-40" y="120"/>
+<use xlink:href="#c" x="-50" y="140"/>
+<use xlink:href="#c" x="-40" y="160"/>
+<use xlink:href="#c" x="-30" y="180"/>
+<use xlink:href="#c" x="-40" y="200"/>
+<use xlink:href="#c" x="-30" y="220"/>
+<use xlink:href="#c" x="-20" y="40"/>
+<use xlink:href="#c" x="-30" y="60"/>
+<use xlink:href="#c" x="-20" y="80"/>
+<use xlink:href="#c" x="-30" y="100"/>
+<use xlink:href="#c" x="-20" y="120"/>
+<use xlink:href="#c" x="-30" y="140"/>
+<use xlink:href="#c" x="-20" y="160"/>
+<use xlink:href="#c" x="-10" y="180"/>
+<use xlink:href="#c" x="-20" y="200"/>
+<use xlink:href="#c" x="-10" y="220"/>
+<use xlink:href="#c" x="-20" y="240"/>
+<use xlink:href="#c" x="-10" y="260"/>
+<use xlink:href="#c" x="-10" y="20"/>
+<use xlink:href="#c" y="40"/>
+<use xlink:href="#c" x="-10" y="60"/>
+<use xlink:href="#c" y="80"/>
+<use xlink:href="#c" x="-10" y="100"/>
+<use xlink:href="#c" y="120"/>
+<use xlink:href="#c" x="-10" y="140"/>
+<use xlink:href="#c" y="160"/>
+<use xlink:href="#c" x="10" y="180"/>
+<use xlink:href="#c" y="200"/>
+<use xlink:href="#c" x="10" y="220"/>
+<use xlink:href="#c" y="240"/>
+<use xlink:href="#c" x="10" y="260"/>
+<use xlink:href="#c" y="280"/>
+<use xlink:href="#c" x="10" y="300"/>
+<use xlink:href="#c" x="20"/>
+<use xlink:href="#c" x="10" y="20"/>
+<use xlink:href="#c" x="20" y="40"/>
+<use xlink:href="#c" x="10" y="60"/>
+<use xlink:href="#c" x="20" y="80"/>
+<use xlink:href="#c" x="10" y="100"/>
+<use xlink:href="#c" x="20" y="120"/>
+<use xlink:href="#c" x="10" y="140"/>
+<use xlink:href="#c" x="20" y="160"/>
+<use xlink:href="#c" x="30" y="180"/>
+<use xlink:href="#c" x="20" y="200"/>
+<use xlink:href="#c" x="30" y="220"/>
+<use xlink:href="#c" x="20" y="240"/>
+<use xlink:href="#c" x="30" y="260"/>
+<use xlink:href="#c" x="20" y="280"/>
+<use xlink:href="#c" x="30" y="300"/>
+<use xlink:href="#c" x="40"/>
+<use xlink:href="#c" x="30" y="20"/>
+<use xlink:href="#c" x="40" y="40"/>
+<use xlink:href="#c" x="30" y="60"/>
+<use xlink:href="#c" x="40" y="80"/>
+<use xlink:href="#c" x="30" y="100"/>
+<use xlink:href="#c" x="40" y="120"/>
+<use xlink:href="#c" x="30" y="140"/>
+<use xlink:href="#c" x="40" y="160"/>
+<use xlink:href="#c" x="50" y="180"/>
+<use xlink:href="#c" x="40" y="200"/>
+<use xlink:href="#c" x="50" y="220"/>
+<use xlink:href="#c" x="40" y="240"/>
+<use xlink:href="#c" x="50" y="260"/>
+<use xlink:href="#c" x="40" y="280"/>
+<use xlink:href="#c" x="50" y="300"/>
+<use xlink:href="#c" x="60"/>
+<use xlink:href="#c" x="50" y="20"/>
+<use xlink:href="#c" x="60" y="40"/>
+<use xlink:href="#c" x="50" y="60"/>
+<use xlink:href="#c" x="60" y="80"/>
+<use xlink:href="#c" x="50" y="100"/>
+<use xlink:href="#c" x="60" y="120"/>
+<use xlink:href="#c" x="50" y="140"/>
+<use xlink:href="#c" x="60" y="160"/>
+<use xlink:href="#c" x="70" y="180"/>
+<use xlink:href="#c" x="60" y="200"/>
+<use xlink:href="#c" x="70" y="220"/>
+<use xlink:href="#c" x="60" y="240"/>
+<use xlink:href="#c" x="70" y="260"/>
+<use xlink:href="#c" x="60" y="280"/>
+<use xlink:href="#c" x="70" y="300"/>
+<use xlink:href="#c" x="80"/>
+<use xlink:href="#c" x="70" y="20"/>
+<use xlink:href="#c" x="80" y="40"/>
+<use xlink:href="#c" x="70" y="60"/>
+<use xlink:href="#c" x="80" y="80"/>
+<use xlink:href="#c" x="70" y="100"/>
+<use xlink:href="#c" x="80" y="120"/>
+<use xlink:href="#c" x="70" y="140"/>
+<use xlink:href="#c" x="80" y="160"/>
+<use xlink:href="#c" x="90" y="180"/>
+<use xlink:href="#c" x="80" y="200"/>
+<use xlink:href="#c" x="90" y="220"/>
+<use xlink:href="#c" x="80" y="240"/>
+<use xlink:href="#c" x="90" y="260"/>
+<use xlink:href="#c" x="80" y="280"/>
+<use xlink:href="#c" x="90" y="300"/>
+<use xlink:href="#c" x="100"/>
+<use xlink:href="#c" x="90" y="20"/>
+<use xlink:href="#c" x="100" y="40"/>
+<use xlink:href="#c" x="90" y="60"/>
+<use xlink:href="#c" x="100" y="80"/>
+<use xlink:href="#c" x="90" y="100"/>
+<use xlink:href="#c" x="100" y="120"/>
+<use xlink:href="#c" x="90" y="140"/>
+<use xlink:href="#c" x="100" y="160"/>
+<use xlink:href="#c" x="110" y="180"/>
+<use xlink:href="#c" x="100" y="200"/>
+<use xlink:href="#c" x="110" y="220"/>
+<use xlink:href="#c" x="100" y="240"/>
+<use xlink:href="#c" x="110" y="260"/>
+<use xlink:href="#c" x="100" y="280"/>
+<use xlink:href="#c" x="110" y="300"/>
+<use xlink:href="#c" x="120"/>
+<use xlink:href="#c" x="110" y="20"/>
+<use xlink:href="#c" x="120" y="40"/>
+<use xlink:href="#c" x="110" y="60"/>
+<use xlink:href="#c" x="120" y="80"/>
+<use xlink:href="#c" x="110" y="100"/>
+<use xlink:href="#c" x="120" y="120"/>
+<use xlink:href="#c" x="110" y="140"/>
+<use xlink:href="#c" x="120" y="160"/>
+<use xlink:href="#c" x="130" y="180"/>
+<use xlink:href="#c" x="120" y="200"/>
+<use xlink:href="#c" x="130" y="220"/>
+<use xlink:href="#c" x="120" y="240"/>
+<use xlink:href="#c" x="130" y="260"/>
+<use xlink:href="#c" x="120" y="280"/>
+<use xlink:href="#c" x="130" y="300"/>
+<use xlink:href="#c" x="140"/>
+<use xlink:href="#c" x="130" y="20"/>
+<use xlink:href="#c" x="140" y="40"/>
+<use xlink:href="#c" x="130" y="60"/>
+<use xlink:href="#c" x="140" y="80"/>
+<use xlink:href="#c" x="130" y="100"/>
+<use xlink:href="#c" x="140" y="120"/>
+<use xlink:href="#c" x="130" y="140"/>
+<use xlink:href="#c" x="140" y="160"/>
+<use xlink:href="#c" x="150" y="180"/>
+<use xlink:href="#c" x="140" y="200"/>
+<use xlink:href="#c" x="150" y="220"/>
+<use xlink:href="#c" x="140" y="240"/>
+<use xlink:href="#c" x="150" y="260"/>
+<use xlink:href="#c" x="140" y="280"/>
+<use xlink:href="#c" x="150" y="300"/>
+<use xlink:href="#c" x="160"/>
+<use xlink:href="#c" x="150" y="20"/>
+<use xlink:href="#c" x="160" y="40"/>
+<use xlink:href="#c" x="150" y="60"/>
+<use xlink:href="#c" x="160" y="80"/>
+<use xlink:href="#c" x="150" y="100"/>
+<use xlink:href="#c" x="160" y="120"/>
+<use xlink:href="#c" x="150" y="140"/>
+<use xlink:href="#c" x="160" y="160"/>
+<use xlink:href="#c" x="170" y="180"/>
+<use xlink:href="#c" x="160" y="200"/>
+<use xlink:href="#c" x="170" y="220"/>
+<use xlink:href="#c" x="160" y="240"/>
+<use xlink:href="#c" x="170" y="260"/>
+<use xlink:href="#c" x="160" y="280"/>
+<use xlink:href="#c" x="170" y="20"/>
+<use xlink:href="#c" x="180" y="40"/>
+<use xlink:href="#c" x="170" y="60"/>
+<use xlink:href="#c" x="180" y="80"/>
+<use xlink:href="#c" x="170" y="100"/>
+<use xlink:href="#c" x="180" y="120"/>
+<use xlink:href="#c" x="170" y="140"/>
+<use xlink:href="#c" x="180" y="160"/>
+<use xlink:href="#c" x="190" y="180"/>
+<use xlink:href="#c" x="180" y="200"/>
+<use xlink:href="#c" x="190" y="220"/>
+<use xlink:href="#c" x="180" y="240"/>
+<use xlink:href="#c" x="190" y="60"/>
+<use xlink:href="#c" x="200" y="80"/>
+<use xlink:href="#c" x="190" y="100"/>
+<use xlink:href="#c" x="200" y="120"/>
+<use xlink:href="#c" x="190" y="140"/>
+<use xlink:href="#c" x="200" y="160"/>
+<use xlink:href="#c" x="210" y="180"/>
+<use xlink:href="#c" x="200" y="200"/>
+<use xlink:href="#c" x="210" y="100"/>
+<use xlink:href="#c" x="220" y="120"/>
+<use xlink:href="#c" x="210" y="140"/>
+<use xlink:href="#c" x="220" y="160"/>
+<use xlink:href="#c" x="230" y="140"/>
+</svg>
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 (file)
index 0000000..ec0dbc3
--- /dev/null
@@ -0,0 +1,496 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="360" width="360" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m90 0-90 180 90 180h180l90-180-90-180z" fill="#494347"/>
+<g id="c">
+<path d="m80 20 1.732-1.155l8.268-16.845v-2z" fill="#3B3639"/>
+<path d="m80 20h20l-10-20v2l8.27 16.845h-16.538z" fill="#6D686B"/>
+</g>
+<g id="d">
+<path d="m110 0-1.732 1.155l-8.268 16.845v2z" fill="#6D686B"/>
+<path d="m110 0h-20l10 20v-2l-8.27-16.845h16.538z" fill="#3B3639"/>
+</g>
+<use xlink:href="#d" x="-90" y="180"/>
+<use xlink:href="#d" x="-80" y="200"/>
+<use xlink:href="#d" x="-70" y="140"/>
+<use xlink:href="#d" x="-80" y="160"/>
+<use xlink:href="#d" x="-70" y="180"/>
+<use xlink:href="#d" x="-60" y="200"/>
+<use xlink:href="#d" x="-70" y="220"/>
+<use xlink:href="#d" x="-60" y="240"/>
+<use xlink:href="#d" x="-50" y="100"/>
+<use xlink:href="#d" x="-60" y="120"/>
+<use xlink:href="#d" x="-50" y="140"/>
+<use xlink:href="#d" x="-60" y="160"/>
+<use xlink:href="#d" x="-50" y="180"/>
+<use xlink:href="#d" x="-40" y="200"/>
+<use xlink:href="#d" x="-50" y="220"/>
+<use xlink:href="#d" x="-40" y="240"/>
+<use xlink:href="#d" x="-50" y="260"/>
+<use xlink:href="#d" x="-40" y="280"/>
+<use xlink:href="#d" x="-30" y="60"/>
+<use xlink:href="#d" x="-40" y="80"/>
+<use xlink:href="#d" x="-30" y="100"/>
+<use xlink:href="#d" x="-40" y="120"/>
+<use xlink:href="#d" x="-30" y="140"/>
+<use xlink:href="#d" x="-40" y="160"/>
+<use xlink:href="#d" x="-30" y="180"/>
+<use xlink:href="#d" x="-20" y="200"/>
+<use xlink:href="#d" x="-30" y="220"/>
+<use xlink:href="#d" x="-20" y="240"/>
+<use xlink:href="#d" x="-30" y="260"/>
+<use xlink:href="#d" x="-20" y="280"/>
+<use xlink:href="#d" x="-30" y="300"/>
+<use xlink:href="#d" x="-20" y="320"/>
+<use xlink:href="#d" x="-10" y="20"/>
+<use xlink:href="#d" x="-20" y="40"/>
+<use xlink:href="#d" x="-10" y="60"/>
+<use xlink:href="#d" x="-20" y="80"/>
+<use xlink:href="#d" x="-10" y="100"/>
+<use xlink:href="#d" x="-20" y="120"/>
+<use xlink:href="#d" x="-10" y="140"/>
+<use xlink:href="#d" x="-20" y="160"/>
+<use xlink:href="#d" x="-10" y="180"/>
+<use xlink:href="#d" y="200"/>
+<use xlink:href="#d" x="-10" y="220"/>
+<use xlink:href="#d" y="240"/>
+<use xlink:href="#d" x="-10" y="260"/>
+<use xlink:href="#d" y="280"/>
+<use xlink:href="#d" x="-10" y="300"/>
+<use xlink:href="#d" y="320"/>
+<use xlink:href="#d" x="-10" y="340"/>
+<use xlink:href="#d" x="10" y="20"/>
+<use xlink:href="#d" y="40"/>
+<use xlink:href="#d" x="10" y="60"/>
+<use xlink:href="#d" y="80"/>
+<use xlink:href="#d" x="10" y="100"/>
+<use xlink:href="#d" y="120"/>
+<use xlink:href="#d" x="10" y="140"/>
+<use xlink:href="#d" y="160"/>
+<use xlink:href="#d" x="10" y="180"/>
+<use xlink:href="#d" x="20" y="200"/>
+<use xlink:href="#d" x="10" y="220"/>
+<use xlink:href="#d" x="20" y="240"/>
+<use xlink:href="#d" x="10" y="260"/>
+<use xlink:href="#d" x="20" y="280"/>
+<use xlink:href="#d" x="10" y="300"/>
+<use xlink:href="#d" x="20" y="320"/>
+<use xlink:href="#d" x="10" y="340"/>
+<use xlink:href="#d" x="20"/>
+<use xlink:href="#d" x="30" y="20"/>
+<use xlink:href="#d" x="20" y="40"/>
+<use xlink:href="#d" x="30" y="60"/>
+<use xlink:href="#d" x="20" y="80"/>
+<use xlink:href="#d" x="30" y="100"/>
+<use xlink:href="#d" x="20" y="120"/>
+<use xlink:href="#d" x="30" y="140"/>
+<use xlink:href="#d" x="20" y="160"/>
+<use xlink:href="#d" x="30" y="180"/>
+<use xlink:href="#d" x="40" y="200"/>
+<use xlink:href="#d" x="30" y="220"/>
+<use xlink:href="#d" x="40" y="240"/>
+<use xlink:href="#d" x="30" y="260"/>
+<use xlink:href="#d" x="40" y="280"/>
+<use xlink:href="#d" x="30" y="300"/>
+<use xlink:href="#d" x="40" y="320"/>
+<use xlink:href="#d" x="30" y="340"/>
+<use xlink:href="#d" x="40"/>
+<use xlink:href="#d" x="50" y="20"/>
+<use xlink:href="#d" x="40" y="40"/>
+<use xlink:href="#d" x="50" y="60"/>
+<use xlink:href="#d" x="40" y="80"/>
+<use xlink:href="#d" x="50" y="100"/>
+<use xlink:href="#d" x="40" y="120"/>
+<use xlink:href="#d" x="50" y="140"/>
+<use xlink:href="#d" x="40" y="160"/>
+<use xlink:href="#d" x="50" y="180"/>
+<use xlink:href="#d" x="60" y="200"/>
+<use xlink:href="#d" x="50" y="220"/>
+<use xlink:href="#d" x="60" y="240"/>
+<use xlink:href="#d" x="50" y="260"/>
+<use xlink:href="#d" x="60" y="280"/>
+<use xlink:href="#d" x="50" y="300"/>
+<use xlink:href="#d" x="60" y="320"/>
+<use xlink:href="#d" x="50" y="340"/>
+<use xlink:href="#d" x="60"/>
+<use xlink:href="#d" x="70" y="20"/>
+<use xlink:href="#d" x="60" y="40"/>
+<use xlink:href="#d" x="70" y="60"/>
+<use xlink:href="#d" x="60" y="80"/>
+<use xlink:href="#d" x="70" y="100"/>
+<use xlink:href="#d" x="60" y="120"/>
+<use xlink:href="#d" x="70" y="140"/>
+<use xlink:href="#d" x="60" y="160"/>
+<use xlink:href="#d" x="70" y="180"/>
+<use xlink:href="#d" x="80" y="200"/>
+<use xlink:href="#d" x="70" y="220"/>
+<use xlink:href="#d" x="80" y="240"/>
+<use xlink:href="#d" x="70" y="260"/>
+<use xlink:href="#d" x="80" y="280"/>
+<use xlink:href="#d" x="70" y="300"/>
+<use xlink:href="#d" x="80" y="320"/>
+<use xlink:href="#d" x="70" y="340"/>
+<use xlink:href="#d" x="80"/>
+<use xlink:href="#d" x="90" y="20"/>
+<use xlink:href="#d" x="80" y="40"/>
+<use xlink:href="#d" x="90" y="60"/>
+<use xlink:href="#d" x="80" y="80"/>
+<use xlink:href="#d" x="90" y="100"/>
+<use xlink:href="#d" x="80" y="120"/>
+<use xlink:href="#d" x="90" y="140"/>
+<use xlink:href="#d" x="80" y="160"/>
+<use xlink:href="#d" x="90" y="180"/>
+<use xlink:href="#d" x="100" y="200"/>
+<use xlink:href="#d" x="90" y="220"/>
+<use xlink:href="#d" x="100" y="240"/>
+<use xlink:href="#d" x="90" y="260"/>
+<use xlink:href="#d" x="100" y="280"/>
+<use xlink:href="#d" x="90" y="300"/>
+<use xlink:href="#d" x="100" y="320"/>
+<use xlink:href="#d" x="90" y="340"/>
+<use xlink:href="#d" x="100"/>
+<use xlink:href="#d" x="110" y="20"/>
+<use xlink:href="#d" x="100" y="40"/>
+<use xlink:href="#d" x="110" y="60"/>
+<use xlink:href="#d" x="100" y="80"/>
+<use xlink:href="#d" x="110" y="100"/>
+<use xlink:href="#d" x="100" y="120"/>
+<use xlink:href="#d" x="110" y="140"/>
+<use xlink:href="#d" x="100" y="160"/>
+<use xlink:href="#d" x="110" y="180"/>
+<use xlink:href="#d" x="120" y="200"/>
+<use xlink:href="#d" x="110" y="220"/>
+<use xlink:href="#d" x="120" y="240"/>
+<use xlink:href="#d" x="110" y="260"/>
+<use xlink:href="#d" x="120" y="280"/>
+<use xlink:href="#d" x="110" y="300"/>
+<use xlink:href="#d" x="120" y="320"/>
+<use xlink:href="#d" x="110" y="340"/>
+<use xlink:href="#d" x="120"/>
+<use xlink:href="#d" x="130" y="20"/>
+<use xlink:href="#d" x="120" y="40"/>
+<use xlink:href="#d" x="130" y="60"/>
+<use xlink:href="#d" x="120" y="80"/>
+<use xlink:href="#d" x="130" y="100"/>
+<use xlink:href="#d" x="120" y="120"/>
+<use xlink:href="#d" x="130" y="140"/>
+<use xlink:href="#d" x="120" y="160"/>
+<use xlink:href="#d" x="130" y="180"/>
+<use xlink:href="#d" x="140" y="200"/>
+<use xlink:href="#d" x="130" y="220"/>
+<use xlink:href="#d" x="140" y="240"/>
+<use xlink:href="#d" x="130" y="260"/>
+<use xlink:href="#d" x="140" y="280"/>
+<use xlink:href="#d" x="130" y="300"/>
+<use xlink:href="#d" x="140" y="320"/>
+<use xlink:href="#d" x="130" y="340"/>
+<use xlink:href="#d" x="140"/>
+<use xlink:href="#d" x="150" y="20"/>
+<use xlink:href="#d" x="140" y="40"/>
+<use xlink:href="#d" x="150" y="60"/>
+<use xlink:href="#d" x="140" y="80"/>
+<use xlink:href="#d" x="150" y="100"/>
+<use xlink:href="#d" x="140" y="120"/>
+<use xlink:href="#d" x="150" y="140"/>
+<use xlink:href="#d" x="140" y="160"/>
+<use xlink:href="#d" x="150" y="180"/>
+<use xlink:href="#d" x="160" y="200"/>
+<use xlink:href="#d" x="150" y="220"/>
+<use xlink:href="#d" x="160" y="240"/>
+<use xlink:href="#d" x="150" y="260"/>
+<use xlink:href="#d" x="160" y="280"/>
+<use xlink:href="#d" x="150" y="300"/>
+<use xlink:href="#d" x="160" y="320"/>
+<use xlink:href="#d" x="150" y="340"/>
+<use xlink:href="#d" x="160"/>
+<use xlink:href="#d" x="170" y="20"/>
+<use xlink:href="#d" x="160" y="40"/>
+<use xlink:href="#d" x="170" y="60"/>
+<use xlink:href="#d" x="160" y="80"/>
+<use xlink:href="#d" x="170" y="100"/>
+<use xlink:href="#d" x="160" y="120"/>
+<use xlink:href="#d" x="170" y="140"/>
+<use xlink:href="#d" x="160" y="160"/>
+<use xlink:href="#d" x="170" y="180"/>
+<use xlink:href="#d" x="180" y="200"/>
+<use xlink:href="#d" x="170" y="220"/>
+<use xlink:href="#d" x="180" y="240"/>
+<use xlink:href="#d" x="170" y="260"/>
+<use xlink:href="#d" x="180" y="280"/>
+<use xlink:href="#d" x="170" y="300"/>
+<use xlink:href="#d" x="180" y="320"/>
+<use xlink:href="#d" x="170" y="340"/>
+<use xlink:href="#d" x="180" y="40"/>
+<use xlink:href="#d" x="190" y="60"/>
+<use xlink:href="#d" x="180" y="80"/>
+<use xlink:href="#d" x="190" y="100"/>
+<use xlink:href="#d" x="180" y="120"/>
+<use xlink:href="#d" x="190" y="140"/>
+<use xlink:href="#d" x="180" y="160"/>
+<use xlink:href="#d" x="190" y="180"/>
+<use xlink:href="#d" x="200" y="200"/>
+<use xlink:href="#d" x="190" y="220"/>
+<use xlink:href="#d" x="200" y="240"/>
+<use xlink:href="#d" x="190" y="260"/>
+<use xlink:href="#d" x="200" y="280"/>
+<use xlink:href="#d" x="190" y="300"/>
+<use xlink:href="#d" x="200" y="80"/>
+<use xlink:href="#d" x="210" y="100"/>
+<use xlink:href="#d" x="200" y="120"/>
+<use xlink:href="#d" x="210" y="140"/>
+<use xlink:href="#d" x="200" y="160"/>
+<use xlink:href="#d" x="210" y="180"/>
+<use xlink:href="#d" x="220" y="200"/>
+<use xlink:href="#d" x="210" y="220"/>
+<use xlink:href="#d" x="220" y="240"/>
+<use xlink:href="#d" x="210" y="260"/>
+<use xlink:href="#d" x="220" y="120"/>
+<use xlink:href="#d" x="230" y="140"/>
+<use xlink:href="#d" x="220" y="160"/>
+<use xlink:href="#d" x="230" y="180"/>
+<use xlink:href="#d" x="240" y="200"/>
+<use xlink:href="#d" x="230" y="220"/>
+<use xlink:href="#d" x="240" y="160"/>
+<use xlink:href="#d" x="250" y="180"/>
+<use xlink:href="#c" x="-70" y="140"/>
+<use xlink:href="#c" x="-80" y="160"/>
+<use xlink:href="#c" x="-70" y="180"/>
+<use xlink:href="#c" x="-60" y="200"/>
+<use xlink:href="#c" x="-50" y="100"/>
+<use xlink:href="#c" x="-60" y="120"/>
+<use xlink:href="#c" x="-50" y="140"/>
+<use xlink:href="#c" x="-60" y="160"/>
+<use xlink:href="#c" x="-50" y="180"/>
+<use xlink:href="#c" x="-40" y="200"/>
+<use xlink:href="#c" x="-50" y="220"/>
+<use xlink:href="#c" x="-40" y="240"/>
+<use xlink:href="#c" x="-30" y="60"/>
+<use xlink:href="#c" x="-40" y="80"/>
+<use xlink:href="#c" x="-30" y="100"/>
+<use xlink:href="#c" x="-40" y="120"/>
+<use xlink:href="#c" x="-30" y="140"/>
+<use xlink:href="#c" x="-40" y="160"/>
+<use xlink:href="#c" x="-30" y="180"/>
+<use xlink:href="#c" x="-20" y="200"/>
+<use xlink:href="#c" x="-30" y="220"/>
+<use xlink:href="#c" x="-20" y="240"/>
+<use xlink:href="#c" x="-30" y="260"/>
+<use xlink:href="#c" x="-20" y="280"/>
+<use xlink:href="#c" x="-10" y="20"/>
+<use xlink:href="#c" x="-20" y="40"/>
+<use xlink:href="#c" x="-10" y="60"/>
+<use xlink:href="#c" x="-20" y="80"/>
+<use xlink:href="#c" x="-10" y="100"/>
+<use xlink:href="#c" x="-20" y="120"/>
+<use xlink:href="#c" x="-10" y="140"/>
+<use xlink:href="#c" x="-20" y="160"/>
+<use xlink:href="#c" x="-10" y="180"/>
+<use xlink:href="#c" y="200"/>
+<use xlink:href="#c" x="-10" y="220"/>
+<use xlink:href="#c" y="240"/>
+<use xlink:href="#c" x="-10" y="260"/>
+<use xlink:href="#c" y="280"/>
+<use xlink:href="#c" x="-10" y="300"/>
+<use xlink:href="#c" y="320"/>
+<use xlink:href="#c" x="10" y="20"/>
+<use xlink:href="#c" y="40"/>
+<use xlink:href="#c" x="10" y="60"/>
+<use xlink:href="#c" y="80"/>
+<use xlink:href="#c" x="10" y="100"/>
+<use xlink:href="#c" y="120"/>
+<use xlink:href="#c" x="10" y="140"/>
+<use xlink:href="#c" y="160"/>
+<use xlink:href="#c" x="10" y="180"/>
+<use xlink:href="#c" x="20" y="200"/>
+<use xlink:href="#c" x="10" y="220"/>
+<use xlink:href="#c" x="20" y="240"/>
+<use xlink:href="#c" x="10" y="260"/>
+<use xlink:href="#c" x="20" y="280"/>
+<use xlink:href="#c" x="10" y="300"/>
+<use xlink:href="#c" x="20" y="320"/>
+<use xlink:href="#c" x="10" y="340"/>
+<use xlink:href="#c" x="20"/>
+<use xlink:href="#c" x="30" y="20"/>
+<use xlink:href="#c" x="20" y="40"/>
+<use xlink:href="#c" x="30" y="60"/>
+<use xlink:href="#c" x="20" y="80"/>
+<use xlink:href="#c" x="30" y="100"/>
+<use xlink:href="#c" x="20" y="120"/>
+<use xlink:href="#c" x="30" y="140"/>
+<use xlink:href="#c" x="20" y="160"/>
+<use xlink:href="#c" x="30" y="180"/>
+<use xlink:href="#c" x="40" y="200"/>
+<use xlink:href="#c" x="30" y="220"/>
+<use xlink:href="#c" x="40" y="240"/>
+<use xlink:href="#c" x="30" y="260"/>
+<use xlink:href="#c" x="40" y="280"/>
+<use xlink:href="#c" x="30" y="300"/>
+<use xlink:href="#c" x="40" y="320"/>
+<use xlink:href="#c" x="30" y="340"/>
+<use xlink:href="#c" x="40"/>
+<use xlink:href="#c" x="50" y="20"/>
+<use xlink:href="#c" x="40" y="40"/>
+<use xlink:href="#c" x="50" y="60"/>
+<use xlink:href="#c" x="40" y="80"/>
+<use xlink:href="#c" x="50" y="100"/>
+<use xlink:href="#c" x="40" y="120"/>
+<use xlink:href="#c" x="50" y="140"/>
+<use xlink:href="#c" x="40" y="160"/>
+<use xlink:href="#c" x="50" y="180"/>
+<use xlink:href="#c" x="60" y="200"/>
+<use xlink:href="#c" x="50" y="220"/>
+<use xlink:href="#c" x="60" y="240"/>
+<use xlink:href="#c" x="50" y="260"/>
+<use xlink:href="#c" x="60" y="280"/>
+<use xlink:href="#c" x="50" y="300"/>
+<use xlink:href="#c" x="60" y="320"/>
+<use xlink:href="#c" x="50" y="340"/>
+<use xlink:href="#c" x="60"/>
+<use xlink:href="#c" x="70" y="20"/>
+<use xlink:href="#c" x="60" y="40"/>
+<use xlink:href="#c" x="70" y="60"/>
+<use xlink:href="#c" x="60" y="80"/>
+<use xlink:href="#c" x="70" y="100"/>
+<use xlink:href="#c" x="60" y="120"/>
+<use xlink:href="#c" x="70" y="140"/>
+<use xlink:href="#c" x="60" y="160"/>
+<use xlink:href="#c" x="70" y="180"/>
+<use xlink:href="#c" x="80" y="200"/>
+<use xlink:href="#c" x="70" y="220"/>
+<use xlink:href="#c" x="80" y="240"/>
+<use xlink:href="#c" x="70" y="260"/>
+<use xlink:href="#c" x="80" y="280"/>
+<use xlink:href="#c" x="70" y="300"/>
+<use xlink:href="#c" x="80" y="320"/>
+<use xlink:href="#c" x="70" y="340"/>
+<use xlink:href="#c" x="80"/>
+<use xlink:href="#c" x="90" y="20"/>
+<use xlink:href="#c" x="80" y="40"/>
+<use xlink:href="#c" x="90" y="60"/>
+<use xlink:href="#c" x="80" y="80"/>
+<use xlink:href="#c" x="90" y="100"/>
+<use xlink:href="#c" x="80" y="120"/>
+<use xlink:href="#c" x="90" y="140"/>
+<use xlink:href="#c" x="80" y="160"/>
+<use xlink:href="#c" x="90" y="180"/>
+<use xlink:href="#c" x="100" y="200"/>
+<use xlink:href="#c" x="90" y="220"/>
+<use xlink:href="#c" x="100" y="240"/>
+<use xlink:href="#c" x="90" y="260"/>
+<use xlink:href="#c" x="100" y="280"/>
+<use xlink:href="#c" x="90" y="300"/>
+<use xlink:href="#c" x="100" y="320"/>
+<use xlink:href="#c" x="90" y="340"/>
+<use xlink:href="#c" x="100"/>
+<use xlink:href="#c" x="110" y="20"/>
+<use xlink:href="#c" x="100" y="40"/>
+<use xlink:href="#c" x="110" y="60"/>
+<use xlink:href="#c" x="100" y="80"/>
+<use xlink:href="#c" x="110" y="100"/>
+<use xlink:href="#c" x="100" y="120"/>
+<use xlink:href="#c" x="110" y="140"/>
+<use xlink:href="#c" x="100" y="160"/>
+<use xlink:href="#c" x="110" y="180"/>
+<use xlink:href="#c" x="120" y="200"/>
+<use xlink:href="#c" x="110" y="220"/>
+<use xlink:href="#c" x="120" y="240"/>
+<use xlink:href="#c" x="110" y="260"/>
+<use xlink:href="#c" x="120" y="280"/>
+<use xlink:href="#c" x="110" y="300"/>
+<use xlink:href="#c" x="120" y="320"/>
+<use xlink:href="#c" x="110" y="340"/>
+<use xlink:href="#c" x="120"/>
+<use xlink:href="#c" x="130" y="20"/>
+<use xlink:href="#c" x="120" y="40"/>
+<use xlink:href="#c" x="130" y="60"/>
+<use xlink:href="#c" x="120" y="80"/>
+<use xlink:href="#c" x="130" y="100"/>
+<use xlink:href="#c" x="120" y="120"/>
+<use xlink:href="#c" x="130" y="140"/>
+<use xlink:href="#c" x="120" y="160"/>
+<use xlink:href="#c" x="130" y="180"/>
+<use xlink:href="#c" x="140" y="200"/>
+<use xlink:href="#c" x="130" y="220"/>
+<use xlink:href="#c" x="140" y="240"/>
+<use xlink:href="#c" x="130" y="260"/>
+<use xlink:href="#c" x="140" y="280"/>
+<use xlink:href="#c" x="130" y="300"/>
+<use xlink:href="#c" x="140" y="320"/>
+<use xlink:href="#c" x="130" y="340"/>
+<use xlink:href="#c" x="140"/>
+<use xlink:href="#c" x="150" y="20"/>
+<use xlink:href="#c" x="140" y="40"/>
+<use xlink:href="#c" x="150" y="60"/>
+<use xlink:href="#c" x="140" y="80"/>
+<use xlink:href="#c" x="150" y="100"/>
+<use xlink:href="#c" x="140" y="120"/>
+<use xlink:href="#c" x="150" y="140"/>
+<use xlink:href="#c" x="140" y="160"/>
+<use xlink:href="#c" x="150" y="180"/>
+<use xlink:href="#c" x="160" y="200"/>
+<use xlink:href="#c" x="150" y="220"/>
+<use xlink:href="#c" x="160" y="240"/>
+<use xlink:href="#c" x="150" y="260"/>
+<use xlink:href="#c" x="160" y="280"/>
+<use xlink:href="#c" x="150" y="300"/>
+<use xlink:href="#c" x="160" y="320"/>
+<use xlink:href="#c" x="150" y="340"/>
+<use xlink:href="#c" x="160"/>
+<use xlink:href="#c" x="170" y="20"/>
+<use xlink:href="#c" x="160" y="40"/>
+<use xlink:href="#c" x="170" y="60"/>
+<use xlink:href="#c" x="160" y="80"/>
+<use xlink:href="#c" x="170" y="100"/>
+<use xlink:href="#c" x="160" y="120"/>
+<use xlink:href="#c" x="170" y="140"/>
+<use xlink:href="#c" x="160" y="160"/>
+<use xlink:href="#c" x="170" y="180"/>
+<use xlink:href="#c" x="180" y="200"/>
+<use xlink:href="#c" x="170" y="220"/>
+<use xlink:href="#c" x="180" y="240"/>
+<use xlink:href="#c" x="170" y="260"/>
+<use xlink:href="#c" x="180" y="280"/>
+<use xlink:href="#c" x="170" y="300"/>
+<use xlink:href="#c" x="180" y="320"/>
+<use xlink:href="#c" x="170" y="340"/>
+<use xlink:href="#c" x="180"/>
+<use xlink:href="#c" x="190" y="20"/>
+<use xlink:href="#c" x="180" y="40"/>
+<use xlink:href="#c" x="190" y="60"/>
+<use xlink:href="#c" x="180" y="80"/>
+<use xlink:href="#c" x="190" y="100"/>
+<use xlink:href="#c" x="180" y="120"/>
+<use xlink:href="#c" x="190" y="140"/>
+<use xlink:href="#c" x="180" y="160"/>
+<use xlink:href="#c" x="190" y="180"/>
+<use xlink:href="#c" x="200" y="200"/>
+<use xlink:href="#c" x="190" y="220"/>
+<use xlink:href="#c" x="200" y="240"/>
+<use xlink:href="#c" x="190" y="260"/>
+<use xlink:href="#c" x="200" y="280"/>
+<use xlink:href="#c" x="190" y="300"/>
+<use xlink:href="#c" x="200" y="40"/>
+<use xlink:href="#c" x="210" y="60"/>
+<use xlink:href="#c" x="200" y="80"/>
+<use xlink:href="#c" x="210" y="100"/>
+<use xlink:href="#c" x="200" y="120"/>
+<use xlink:href="#c" x="210" y="140"/>
+<use xlink:href="#c" x="200" y="160"/>
+<use xlink:href="#c" x="210" y="180"/>
+<use xlink:href="#c" x="220" y="200"/>
+<use xlink:href="#c" x="210" y="220"/>
+<use xlink:href="#c" x="220" y="240"/>
+<use xlink:href="#c" x="210" y="260"/>
+<use xlink:href="#c" x="220" y="80"/>
+<use xlink:href="#c" x="230" y="100"/>
+<use xlink:href="#c" x="220" y="120"/>
+<use xlink:href="#c" x="230" y="140"/>
+<use xlink:href="#c" x="220" y="160"/>
+<use xlink:href="#c" x="230" y="180"/>
+<use xlink:href="#c" x="240" y="200"/>
+<use xlink:href="#c" x="230" y="220"/>
+<use xlink:href="#c" x="240" y="120"/>
+<use xlink:href="#c" x="250" y="140"/>
+<use xlink:href="#c" x="240" y="160"/>
+<use xlink:href="#c" x="250" y="180"/>
+<use xlink:href="#c" x="260" y="160"/>
+</svg>
diff --git a/src/pentobi_qml/qml/themes/light/Theme.qml b/src/pentobi_qml/qml/themes/light/Theme.qml
new file mode 100644 (file)
index 0000000..0950ec6
--- /dev/null
@@ -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 (file)
index 0000000..c7d2e5d
--- /dev/null
@@ -0,0 +1,156 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="224" width="224" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<g id="a" transform="translate(98)">
+<rect height="14" width="14" fill="#9a9298"/>
+<path d="m0.5 13.5v-13h13l-0.5 0.5h-12v12z" fill="#767074"/>
+<path d="m0.5 13.5h13v-13l-0.5 0.5v12h-12z" fill="#b2acb0"/>
+</g>
+<use xlink:href="#a" transform="translate(-98 98)"/>
+<use xlink:href="#a" transform="translate(-98 112)"/>
+<use xlink:href="#a" transform="translate(-84 84)"/>
+<use xlink:href="#a" transform="translate(-84 98)"/>
+<use xlink:href="#a" transform="translate(-84 112)"/>
+<use xlink:href="#a" transform="translate(-84 126)"/>
+<use xlink:href="#a" transform="translate(-70 70)"/>
+<use xlink:href="#a" transform="translate(-70 84)"/>
+<use xlink:href="#a" transform="translate(-70 98)"/>
+<use xlink:href="#a" transform="translate(-70 112)"/>
+<use xlink:href="#a" transform="translate(-70 126)"/>
+<use xlink:href="#a" transform="translate(-70 140)"/>
+<use xlink:href="#a" transform="translate(-56 56)"/>
+<use xlink:href="#a" transform="translate(-56 70)"/>
+<use xlink:href="#a" transform="translate(-56 84)"/>
+<use xlink:href="#a" transform="translate(-56 98)"/>
+<use xlink:href="#a" transform="translate(-56 112)"/>
+<use xlink:href="#a" transform="translate(-56 126)"/>
+<use xlink:href="#a" transform="translate(-56 140)"/>
+<use xlink:href="#a" transform="translate(-56 154)"/>
+<use xlink:href="#a" transform="translate(-42 42)"/>
+<use xlink:href="#a" transform="translate(-42 56)"/>
+<use xlink:href="#a" transform="translate(-42 70)"/>
+<use xlink:href="#a" transform="translate(-42 84)"/>
+<use xlink:href="#a" transform="translate(-42 98)"/>
+<use xlink:href="#a" transform="translate(-42 112)"/>
+<use xlink:href="#a" transform="translate(-42 126)"/>
+<use xlink:href="#a" transform="translate(-42 140)"/>
+<use xlink:href="#a" transform="translate(-42 154)"/>
+<use xlink:href="#a" transform="translate(-42 168)"/>
+<use xlink:href="#a" transform="translate(-28 28)"/>
+<use xlink:href="#a" transform="translate(-28 42)"/>
+<use xlink:href="#a" transform="translate(-28 56)"/>
+<use xlink:href="#a" transform="translate(-28 70)"/>
+<use xlink:href="#a" transform="translate(-28 84)"/>
+<use xlink:href="#a" transform="translate(-28 126)"/>
+<use xlink:href="#a" transform="translate(-28 140)"/>
+<use xlink:href="#a" transform="translate(-28 154)"/>
+<use xlink:href="#a" transform="translate(-28 168)"/>
+<use xlink:href="#a" transform="translate(-28 182)"/>
+<use xlink:href="#a" transform="translate(-14 14)"/>
+<use xlink:href="#a" transform="translate(-14 28)"/>
+<use xlink:href="#a" transform="translate(-14 42)"/>
+<use xlink:href="#a" transform="translate(-14 56)"/>
+<use xlink:href="#a" transform="translate(-14 70)"/>
+<use xlink:href="#a" transform="translate(-14 140)"/>
+<use xlink:href="#a" transform="translate(-14 154)"/>
+<use xlink:href="#a" transform="translate(-14 168)"/>
+<use xlink:href="#a" transform="translate(-14 182)"/>
+<use xlink:href="#a" transform="translate(-14 196)"/>
+<use xlink:href="#a" transform="translate(0 14)"/>
+<use xlink:href="#a" transform="translate(0 28)"/>
+<use xlink:href="#a" transform="translate(0 42)"/>
+<use xlink:href="#a" transform="translate(0 56)"/>
+<use xlink:href="#a" transform="translate(0 154)"/>
+<use xlink:href="#a" transform="translate(0 168)"/>
+<use xlink:href="#a" transform="translate(0 182)"/>
+<use xlink:href="#a" transform="translate(0 196)"/>
+<use xlink:href="#a" transform="translate(0 210)"/>
+<use xlink:href="#a" transform="translate(14)"/>
+<use xlink:href="#a" transform="translate(14 14)"/>
+<use xlink:href="#a" transform="translate(14 28)"/>
+<use xlink:href="#a" transform="translate(14 42)"/>
+<use xlink:href="#a" transform="translate(14 56)"/>
+<use xlink:href="#a" transform="translate(14 154)"/>
+<use xlink:href="#a" transform="translate(14 168)"/>
+<use xlink:href="#a" transform="translate(14 182)"/>
+<use xlink:href="#a" transform="translate(14 196)"/>
+<use xlink:href="#a" transform="translate(14 210)"/>
+<use xlink:href="#a" transform="translate(28 14)"/>
+<use xlink:href="#a" transform="translate(28 28)"/>
+<use xlink:href="#a" transform="translate(28 42)"/>
+<use xlink:href="#a" transform="translate(28 56)"/>
+<use xlink:href="#a" transform="translate(28 70)"/>
+<use xlink:href="#a" transform="translate(28 140)"/>
+<use xlink:href="#a" transform="translate(28 154)"/>
+<use xlink:href="#a" transform="translate(28 168)"/>
+<use xlink:href="#a" transform="translate(28 182)"/>
+<use xlink:href="#a" transform="translate(28 196)"/>
+<use xlink:href="#a" transform="translate(42 28)"/>
+<use xlink:href="#a" transform="translate(42 42)"/>
+<use xlink:href="#a" transform="translate(42 56)"/>
+<use xlink:href="#a" transform="translate(42 70)"/>
+<use xlink:href="#a" transform="translate(42 84)"/>
+<use xlink:href="#a" transform="translate(42 126)"/>
+<use xlink:href="#a" transform="translate(42 140)"/>
+<use xlink:href="#a" transform="translate(42 154)"/>
+<use xlink:href="#a" transform="translate(42 168)"/>
+<use xlink:href="#a" transform="translate(42 182)"/>
+<use xlink:href="#a" transform="translate(56 42)"/>
+<use xlink:href="#a" transform="translate(56 56)"/>
+<use xlink:href="#a" transform="translate(56 70)"/>
+<use xlink:href="#a" transform="translate(56 84)"/>
+<use xlink:href="#a" transform="translate(56 98)"/>
+<use xlink:href="#a" transform="translate(56 112)"/>
+<use xlink:href="#a" transform="translate(56 126)"/>
+<use xlink:href="#a" transform="translate(56 140)"/>
+<use xlink:href="#a" transform="translate(56 154)"/>
+<use xlink:href="#a" transform="translate(56 168)"/>
+<use xlink:href="#a" transform="translate(70 56)"/>
+<use xlink:href="#a" transform="translate(70 70)"/>
+<use xlink:href="#a" transform="translate(70 84)"/>
+<use xlink:href="#a" transform="translate(70 98)"/>
+<use xlink:href="#a" transform="translate(70 112)"/>
+<use xlink:href="#a" transform="translate(70 126)"/>
+<use xlink:href="#a" transform="translate(70 140)"/>
+<use xlink:href="#a" transform="translate(70 154)"/>
+<use xlink:href="#a" transform="translate(84 70)"/>
+<use xlink:href="#a" transform="translate(84 84)"/>
+<use xlink:href="#a" transform="translate(84 98)"/>
+<use xlink:href="#a" transform="translate(84 112)"/>
+<use xlink:href="#a" transform="translate(84 126)"/>
+<use xlink:href="#a" transform="translate(84 140)"/>
+<use xlink:href="#a" transform="translate(98 84)"/>
+<use xlink:href="#a" transform="translate(98 98)"/>
+<use xlink:href="#a" transform="translate(98 112)"/>
+<use xlink:href="#a" transform="translate(98 126)"/>
+<use xlink:href="#a" transform="translate(112,98)"/>
+<use xlink:href="#a" transform="translate(112,112)"/>
+<g id="b" transform="translate(98 70)">
+<rect height="14" width="14" fill="#9a9298"/>
+<rect height="12" width="12" y="1" x="1" fill="#696267"/>
+<path d="m0.5 13.5v-13h13l-0.5 0.5h-12v12z" fill="#5a5458"/>
+<path d="m0.5 13.5h13v-13l-0.5 0.5v12h-12z" fill="#797276"/>
+</g>
+<use xlink:href="#b" transform="translate(-28,28)"/>
+<use xlink:href="#b" transform="translate(-28,42)"/>
+<use xlink:href="#b" transform="translate(-14,14)"/>
+<use xlink:href="#b" transform="translate(-14,28)"/>
+<use xlink:href="#b" transform="translate(-14,42)"/>
+<use xlink:href="#b" transform="translate(-14,56)"/>
+<use xlink:href="#b" transform="translate(0,14)"/>
+<use xlink:href="#b" transform="translate(0,28)"/>
+<use xlink:href="#b" transform="translate(0,42)"/>
+<use xlink:href="#b" transform="translate(0,56)"/>
+<use xlink:href="#b" transform="translate(0,70)"/>
+<use xlink:href="#b" transform="translate(14)"/>
+<use xlink:href="#b" transform="translate(14,14)"/>
+<use xlink:href="#b" transform="translate(14,28)"/>
+<use xlink:href="#b" transform="translate(14,42)"/>
+<use xlink:href="#b" transform="translate(14,56)"/>
+<use xlink:href="#b" transform="translate(14,70)"/>
+<use xlink:href="#b" transform="translate(28,14)"/>
+<use xlink:href="#b" transform="translate(28,28)"/>
+<use xlink:href="#b" transform="translate(28,42)"/>
+<use xlink:href="#b" transform="translate(28,56)"/>
+<use xlink:href="#b" transform="translate(42,28)"/>
+<use xlink:href="#b" transform="translate(42,42)"/>
+</svg>
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 (file)
index 0000000..bb0aeb3
--- /dev/null
@@ -0,0 +1,232 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="280" width="280" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<g id="a" transform="translate(126)">
+<rect height="14" width="14" fill="#9a9298"/>
+<path d="m0.5 13.5v-13h13l-0.5 0.5h-12v12z" fill="#767074"/>
+<path d="m0.5 13.5h13v-13l-0.5 0.5v12h-12z" fill="#b2acb0"/>
+</g>
+<use xlink:href="#a" transform="translate(-126,126)"/>
+<use xlink:href="#a" transform="translate(-126,140)"/>
+<use xlink:href="#a" transform="translate(-112,112)"/>
+<use xlink:href="#a" transform="translate(-112,126)"/>
+<use xlink:href="#a" transform="translate(-112,140)"/>
+<use xlink:href="#a" transform="translate(-112,154)"/>
+<use xlink:href="#a" transform="translate(-98 98)"/>
+<use xlink:href="#a" transform="translate(-98 112)"/>
+<use xlink:href="#a" transform="translate(-98 126)"/>
+<use xlink:href="#a" transform="translate(-98 140)"/>
+<use xlink:href="#a" transform="translate(-98 154)"/>
+<use xlink:href="#a" transform="translate(-98 168)"/>
+<use xlink:href="#a" transform="translate(-84 84)"/>
+<use xlink:href="#a" transform="translate(-84 98)"/>
+<use xlink:href="#a" transform="translate(-84 112)"/>
+<use xlink:href="#a" transform="translate(-84 126)"/>
+<use xlink:href="#a" transform="translate(-84 140)"/>
+<use xlink:href="#a" transform="translate(-84 154)"/>
+<use xlink:href="#a" transform="translate(-84 168)"/>
+<use xlink:href="#a" transform="translate(-84 182)"/>
+<use xlink:href="#a" transform="translate(-70 70)"/>
+<use xlink:href="#a" transform="translate(-70 84)"/>
+<use xlink:href="#a" transform="translate(-70 98)"/>
+<use xlink:href="#a" transform="translate(-70 112)"/>
+<use xlink:href="#a" transform="translate(-70 126)"/>
+<use xlink:href="#a" transform="translate(-70 140)"/>
+<use xlink:href="#a" transform="translate(-70 154)"/>
+<use xlink:href="#a" transform="translate(-70 168)"/>
+<use xlink:href="#a" transform="translate(-70 182)"/>
+<use xlink:href="#a" transform="translate(-70 196)"/>
+<use xlink:href="#a" transform="translate(-56 56)"/>
+<use xlink:href="#a" transform="translate(-56 70)"/>
+<use xlink:href="#a" transform="translate(-56 84)"/>
+<use xlink:href="#a" transform="translate(-56 98)"/>
+<use xlink:href="#a" transform="translate(-56 112)"/>
+<use xlink:href="#a" transform="translate(-56 126)"/>
+<use xlink:href="#a" transform="translate(-56 140)"/>
+<use xlink:href="#a" transform="translate(-56 154)"/>
+<use xlink:href="#a" transform="translate(-56 168)"/>
+<use xlink:href="#a" transform="translate(-56 182)"/>
+<use xlink:href="#a" transform="translate(-56 196)"/>
+<use xlink:href="#a" transform="translate(-56 210)"/>
+<use xlink:href="#a" transform="translate(-42 42)"/>
+<use xlink:href="#a" transform="translate(-42 56)"/>
+<use xlink:href="#a" transform="translate(-42 70)"/>
+<use xlink:href="#a" transform="translate(-42 84)"/>
+<use xlink:href="#a" transform="translate(-42 98)"/>
+<use xlink:href="#a" transform="translate(-42 112)"/>
+<use xlink:href="#a" transform="translate(-42 126)"/>
+<use xlink:href="#a" transform="translate(-42 140)"/>
+<use xlink:href="#a" transform="translate(-42 154)"/>
+<use xlink:href="#a" transform="translate(-42 168)"/>
+<use xlink:href="#a" transform="translate(-42 182)"/>
+<use xlink:href="#a" transform="translate(-42 196)"/>
+<use xlink:href="#a" transform="translate(-42 210)"/>
+<use xlink:href="#a" transform="translate(-42 224)"/>
+<use xlink:href="#a" transform="translate(-28 28)"/>
+<use xlink:href="#a" transform="translate(-28 42)"/>
+<use xlink:href="#a" transform="translate(-28 56)"/>
+<use xlink:href="#a" transform="translate(-28 70)"/>
+<use xlink:href="#a" transform="translate(-28 84)"/>
+<use xlink:href="#a" transform="translate(-28 98)"/>
+<use xlink:href="#a" transform="translate(-28 112)"/>
+<use xlink:href="#a" transform="translate(-28 154)"/>
+<use xlink:href="#a" transform="translate(-28 168)"/>
+<use xlink:href="#a" transform="translate(-28 182)"/>
+<use xlink:href="#a" transform="translate(-28 196)"/>
+<use xlink:href="#a" transform="translate(-28 210)"/>
+<use xlink:href="#a" transform="translate(-28 224)"/>
+<use xlink:href="#a" transform="translate(-28 238)"/>
+<use xlink:href="#a" transform="translate(-14 14)"/>
+<use xlink:href="#a" transform="translate(-14 28)"/>
+<use xlink:href="#a" transform="translate(-14 42)"/>
+<use xlink:href="#a" transform="translate(-14 56)"/>
+<use xlink:href="#a" transform="translate(-14 70)"/>
+<use xlink:href="#a" transform="translate(-14 84)"/>
+<use xlink:href="#a" transform="translate(-14 98)"/>
+<use xlink:href="#a" transform="translate(-14 168)"/>
+<use xlink:href="#a" transform="translate(-14 182)"/>
+<use xlink:href="#a" transform="translate(-14 196)"/>
+<use xlink:href="#a" transform="translate(-14 210)"/>
+<use xlink:href="#a" transform="translate(-14 224)"/>
+<use xlink:href="#a" transform="translate(-14 238)"/>
+<use xlink:href="#a" transform="translate(-14 252)"/>
+<use xlink:href="#a" transform="translate(0 14)"/>
+<use xlink:href="#a" transform="translate(0 28)"/>
+<use xlink:href="#a" transform="translate(0 42)"/>
+<use xlink:href="#a" transform="translate(0 56)"/>
+<use xlink:href="#a" transform="translate(0 70)"/>
+<use xlink:href="#a" transform="translate(0 84)"/>
+<use xlink:href="#a" transform="translate(0 182)"/>
+<use xlink:href="#a" transform="translate(0 196)"/>
+<use xlink:href="#a" transform="translate(0 210)"/>
+<use xlink:href="#a" transform="translate(0 224)"/>
+<use xlink:href="#a" transform="translate(0 238)"/>
+<use xlink:href="#a" transform="translate(0 252)"/>
+<use xlink:href="#a" transform="translate(0 266)"/>
+<use xlink:href="#a" transform="translate(14)"/>
+<use xlink:href="#a" transform="translate(14 14)"/>
+<use xlink:href="#a" transform="translate(14 28)"/>
+<use xlink:href="#a" transform="translate(14 42)"/>
+<use xlink:href="#a" transform="translate(14 56)"/>
+<use xlink:href="#a" transform="translate(14 70)"/>
+<use xlink:href="#a" transform="translate(14 84)"/>
+<use xlink:href="#a" transform="translate(14 182)"/>
+<use xlink:href="#a" transform="translate(14 196)"/>
+<use xlink:href="#a" transform="translate(14 210)"/>
+<use xlink:href="#a" transform="translate(14 224)"/>
+<use xlink:href="#a" transform="translate(14 238)"/>
+<use xlink:href="#a" transform="translate(14 252)"/>
+<use xlink:href="#a" transform="translate(14 266)"/>
+<use xlink:href="#a" transform="translate(28 14)"/>
+<use xlink:href="#a" transform="translate(28 28)"/>
+<use xlink:href="#a" transform="translate(28 42)"/>
+<use xlink:href="#a" transform="translate(28 56)"/>
+<use xlink:href="#a" transform="translate(28 70)"/>
+<use xlink:href="#a" transform="translate(28 84)"/>
+<use xlink:href="#a" transform="translate(28 98)"/>
+<use xlink:href="#a" transform="translate(28 168)"/>
+<use xlink:href="#a" transform="translate(28 182)"/>
+<use xlink:href="#a" transform="translate(28 196)"/>
+<use xlink:href="#a" transform="translate(28 210)"/>
+<use xlink:href="#a" transform="translate(28 224)"/>
+<use xlink:href="#a" transform="translate(28 238)"/>
+<use xlink:href="#a" transform="translate(28 252)"/>
+<use xlink:href="#a" transform="translate(42 28)"/>
+<use xlink:href="#a" transform="translate(42 42)"/>
+<use xlink:href="#a" transform="translate(42 56)"/>
+<use xlink:href="#a" transform="translate(42 70)"/>
+<use xlink:href="#a" transform="translate(42 84)"/>
+<use xlink:href="#a" transform="translate(42 98)"/>
+<use xlink:href="#a" transform="translate(42 112)"/>
+<use xlink:href="#a" transform="translate(42 154)"/>
+<use xlink:href="#a" transform="translate(42 168)"/>
+<use xlink:href="#a" transform="translate(42 182)"/>
+<use xlink:href="#a" transform="translate(42 196)"/>
+<use xlink:href="#a" transform="translate(42 210)"/>
+<use xlink:href="#a" transform="translate(42 224)"/>
+<use xlink:href="#a" transform="translate(42 238)"/>
+<use xlink:href="#a" transform="translate(56 42)"/>
+<use xlink:href="#a" transform="translate(56 56)"/>
+<use xlink:href="#a" transform="translate(56 70)"/>
+<use xlink:href="#a" transform="translate(56 84)"/>
+<use xlink:href="#a" transform="translate(56 98)"/>
+<use xlink:href="#a" transform="translate(56 112)"/>
+<use xlink:href="#a" transform="translate(56 126)"/>
+<use xlink:href="#a" transform="translate(56 140)"/>
+<use xlink:href="#a" transform="translate(56 154)"/>
+<use xlink:href="#a" transform="translate(56 168)"/>
+<use xlink:href="#a" transform="translate(56 182)"/>
+<use xlink:href="#a" transform="translate(56 196)"/>
+<use xlink:href="#a" transform="translate(56 210)"/>
+<use xlink:href="#a" transform="translate(56 224)"/>
+<use xlink:href="#a" transform="translate(70 56)"/>
+<use xlink:href="#a" transform="translate(70 70)"/>
+<use xlink:href="#a" transform="translate(70 84)"/>
+<use xlink:href="#a" transform="translate(70 98)"/>
+<use xlink:href="#a" transform="translate(70 112)"/>
+<use xlink:href="#a" transform="translate(70 126)"/>
+<use xlink:href="#a" transform="translate(70 140)"/>
+<use xlink:href="#a" transform="translate(70 154)"/>
+<use xlink:href="#a" transform="translate(70 168)"/>
+<use xlink:href="#a" transform="translate(70 182)"/>
+<use xlink:href="#a" transform="translate(70 196)"/>
+<use xlink:href="#a" transform="translate(70 210)"/>
+<use xlink:href="#a" transform="translate(84 70)"/>
+<use xlink:href="#a" transform="translate(84 84)"/>
+<use xlink:href="#a" transform="translate(84 98)"/>
+<use xlink:href="#a" transform="translate(84 112)"/>
+<use xlink:href="#a" transform="translate(84 126)"/>
+<use xlink:href="#a" transform="translate(84 140)"/>
+<use xlink:href="#a" transform="translate(84 154)"/>
+<use xlink:href="#a" transform="translate(84 168)"/>
+<use xlink:href="#a" transform="translate(84 182)"/>
+<use xlink:href="#a" transform="translate(84 196)"/>
+<use xlink:href="#a" transform="translate(98 84)"/>
+<use xlink:href="#a" transform="translate(98 98)"/>
+<use xlink:href="#a" transform="translate(98 112)"/>
+<use xlink:href="#a" transform="translate(98 126)"/>
+<use xlink:href="#a" transform="translate(98 140)"/>
+<use xlink:href="#a" transform="translate(98 154)"/>
+<use xlink:href="#a" transform="translate(98 168)"/>
+<use xlink:href="#a" transform="translate(98 182)"/>
+<use xlink:href="#a" transform="translate(112,98)"/>
+<use xlink:href="#a" transform="translate(112,112)"/>
+<use xlink:href="#a" transform="translate(112,126)"/>
+<use xlink:href="#a" transform="translate(112,140)"/>
+<use xlink:href="#a" transform="translate(112,154)"/>
+<use xlink:href="#a" transform="translate(112,168)"/>
+<use xlink:href="#a" transform="translate(126,112)"/>
+<use xlink:href="#a" transform="translate(126,126)"/>
+<use xlink:href="#a" transform="translate(126,140)"/>
+<use xlink:href="#a" transform="translate(126,154)"/>
+<use xlink:href="#a" transform="translate(140,126)"/>
+<use xlink:href="#a" transform="translate(140,140)"/>
+<g id="b" transform="translate(126 98)">
+<rect height="14" width="14" fill="#9a9298"/>
+<rect height="12" width="12" y="1" x="1" fill="#696267"/>
+<path d="m0.5 13.5v-13h13l-0.5 0.5h-12v12z" fill="#5a5458"/>
+<path d="m0.5 13.5h13v-13l-0.5 0.5v12h-12z" fill="#797276"/>
+</g>
+<use xlink:href="#b" transform="translate(-28,28)"/>
+<use xlink:href="#b" transform="translate(-28,42)"/>
+<use xlink:href="#b" transform="translate(-14,14)"/>
+<use xlink:href="#b" transform="translate(-14,28)"/>
+<use xlink:href="#b" transform="translate(-14,42)"/>
+<use xlink:href="#b" transform="translate(-14,56)"/>
+<use xlink:href="#b" transform="translate(0,14)"/>
+<use xlink:href="#b" transform="translate(0,28)"/>
+<use xlink:href="#b" transform="translate(0,42)"/>
+<use xlink:href="#b" transform="translate(0,56)"/>
+<use xlink:href="#b" transform="translate(0,70)"/>
+<use xlink:href="#b" transform="translate(14)"/>
+<use xlink:href="#b" transform="translate(14,14)"/>
+<use xlink:href="#b" transform="translate(14,28)"/>
+<use xlink:href="#b" transform="translate(14,42)"/>
+<use xlink:href="#b" transform="translate(14,56)"/>
+<use xlink:href="#b" transform="translate(14,70)"/>
+<use xlink:href="#b" transform="translate(28,14)"/>
+<use xlink:href="#b" transform="translate(28,28)"/>
+<use xlink:href="#b" transform="translate(28,42)"/>
+<use xlink:href="#b" transform="translate(28,56)"/>
+<use xlink:href="#b" transform="translate(42,28)"/>
+<use xlink:href="#b" transform="translate(42,42)"/>
+</svg>
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 (file)
index 0000000..4338a7c
--- /dev/null
@@ -0,0 +1,300 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="280" width="280" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<g id="a" transform="translate(98)">
+<rect height="14" width="14" fill="#9a9298"/>
+<path d="m0.5 13.5v-13h13l-0.5 0.5h-12v12z" fill="#767074"/>
+<path d="m0.5 13.5h13v-13l-0.5 0.5v12h-12z" fill="#b2acb0"/>
+</g>
+<use xlink:href="#a" transform="translate(-98 98)"/>
+<use xlink:href="#a" transform="translate(-98,112)"/>
+<use xlink:href="#a" transform="translate(-98,126)"/>
+<use xlink:href="#a" transform="translate(-98,140)"/>
+<use xlink:href="#a" transform="translate(-98,154)"/>
+<use xlink:href="#a" transform="translate(-98,168)"/>
+<use xlink:href="#a" transform="translate(-84 84)"/>
+<use xlink:href="#a" transform="translate(-84 98)"/>
+<use xlink:href="#a" transform="translate(-84,112)"/>
+<use xlink:href="#a" transform="translate(-84,126)"/>
+<use xlink:href="#a" transform="translate(-84,140)"/>
+<use xlink:href="#a" transform="translate(-84,154)"/>
+<use xlink:href="#a" transform="translate(-84,168)"/>
+<use xlink:href="#a" transform="translate(-84,182)"/>
+<use xlink:href="#a" transform="translate(-70 70)"/>
+<use xlink:href="#a" transform="translate(-70 84)"/>
+<use xlink:href="#a" transform="translate(-70 98)"/>
+<use xlink:href="#a" transform="translate(-70,112)"/>
+<use xlink:href="#a" transform="translate(-70,126)"/>
+<use xlink:href="#a" transform="translate(-70,140)"/>
+<use xlink:href="#a" transform="translate(-70,154)"/>
+<use xlink:href="#a" transform="translate(-70,168)"/>
+<use xlink:href="#a" transform="translate(-70,182)"/>
+<use xlink:href="#a" transform="translate(-70,196)"/>
+<use xlink:href="#a" transform="translate(-56,56)"/>
+<use xlink:href="#a" transform="translate(-56 70)"/>
+<use xlink:href="#a" transform="translate(-56 84)"/>
+<use xlink:href="#a" transform="translate(-56 98)"/>
+<use xlink:href="#a" transform="translate(-56,112)"/>
+<use xlink:href="#a" transform="translate(-56,126)"/>
+<use xlink:href="#a" transform="translate(-56,140)"/>
+<use xlink:href="#a" transform="translate(-56,154)"/>
+<use xlink:href="#a" transform="translate(-56,168)"/>
+<use xlink:href="#a" transform="translate(-56,182)"/>
+<use xlink:href="#a" transform="translate(-56,196)"/>
+<use xlink:href="#a" transform="translate(-56,210)"/>
+<use xlink:href="#a" transform="translate(-42 42)"/>
+<use xlink:href="#a" transform="translate(-42 56)"/>
+<use xlink:href="#a" transform="translate(-42 70)"/>
+<use xlink:href="#a" transform="translate(-42 84)"/>
+<use xlink:href="#a" transform="translate(-42 98)"/>
+<use xlink:href="#a" transform="translate(-42 112)"/>
+<use xlink:href="#a" transform="translate(-42 126)"/>
+<use xlink:href="#a" transform="translate(-42 140)"/>
+<use xlink:href="#a" transform="translate(-42 154)"/>
+<use xlink:href="#a" transform="translate(-42 168)"/>
+<use xlink:href="#a" transform="translate(-42 182)"/>
+<use xlink:href="#a" transform="translate(-42 196)"/>
+<use xlink:href="#a" transform="translate(-42 210)"/>
+<use xlink:href="#a" transform="translate(-42 224)"/>
+<use xlink:href="#a" transform="translate(-28 28)"/>
+<use xlink:href="#a" transform="translate(-28 42)"/>
+<use xlink:href="#a" transform="translate(-28 56)"/>
+<use xlink:href="#a" transform="translate(-28 70)"/>
+<use xlink:href="#a" transform="translate(-28 84)"/>
+<use xlink:href="#a" transform="translate(-28 98)"/>
+<use xlink:href="#a" transform="translate(-28 112)"/>
+<use xlink:href="#a" transform="translate(-28 126)"/>
+<use xlink:href="#a" transform="translate(-28 140)"/>
+<use xlink:href="#a" transform="translate(-28 154)"/>
+<use xlink:href="#a" transform="translate(-28 168)"/>
+<use xlink:href="#a" transform="translate(-28 182)"/>
+<use xlink:href="#a" transform="translate(-28 196)"/>
+<use xlink:href="#a" transform="translate(-28 210)"/>
+<use xlink:href="#a" transform="translate(-28 224)"/>
+<use xlink:href="#a" transform="translate(-28 238)"/>
+<use xlink:href="#a" transform="translate(-14 14)"/>
+<use xlink:href="#a" transform="translate(-14 28)"/>
+<use xlink:href="#a" transform="translate(-14 42)"/>
+<use xlink:href="#a" transform="translate(-14 56)"/>
+<use xlink:href="#a" transform="translate(-14 70)"/>
+<use xlink:href="#a" transform="translate(-14 84)"/>
+<use xlink:href="#a" transform="translate(-14 98)"/>
+<use xlink:href="#a" transform="translate(-14 112)"/>
+<use xlink:href="#a" transform="translate(-14 126)"/>
+<use xlink:href="#a" transform="translate(-14 140)"/>
+<use xlink:href="#a" transform="translate(-14 154)"/>
+<use xlink:href="#a" transform="translate(-14 168)"/>
+<use xlink:href="#a" transform="translate(-14 182)"/>
+<use xlink:href="#a" transform="translate(-14 196)"/>
+<use xlink:href="#a" transform="translate(-14 210)"/>
+<use xlink:href="#a" transform="translate(-14 224)"/>
+<use xlink:href="#a" transform="translate(-14 238)"/>
+<use xlink:href="#a" transform="translate(-14 252)"/>
+<use xlink:href="#a" transform="translate(0 14)"/>
+<use xlink:href="#a" transform="translate(0 28)"/>
+<use xlink:href="#a" transform="translate(0 42)"/>
+<use xlink:href="#a" transform="translate(0 56)"/>
+<use xlink:href="#a" transform="translate(0 70)"/>
+<use xlink:href="#a" transform="translate(0 84)"/>
+<use xlink:href="#a" transform="translate(0 98)"/>
+<use xlink:href="#a" transform="translate(0 112)"/>
+<use xlink:href="#a" transform="translate(0 154)"/>
+<use xlink:href="#a" transform="translate(0 168)"/>
+<use xlink:href="#a" transform="translate(0 182)"/>
+<use xlink:href="#a" transform="translate(0 196)"/>
+<use xlink:href="#a" transform="translate(0 210)"/>
+<use xlink:href="#a" transform="translate(0 224)"/>
+<use xlink:href="#a" transform="translate(0 238)"/>
+<use xlink:href="#a" transform="translate(0 252)"/>
+<use xlink:href="#a" transform="translate(0 266)"/>
+<use xlink:href="#a" transform="translate(14)"/>
+<use xlink:href="#a" transform="translate(14 14)"/>
+<use xlink:href="#a" transform="translate(14 28)"/>
+<use xlink:href="#a" transform="translate(14 42)"/>
+<use xlink:href="#a" transform="translate(14 56)"/>
+<use xlink:href="#a" transform="translate(14 70)"/>
+<use xlink:href="#a" transform="translate(14 84)"/>
+<use xlink:href="#a" transform="translate(14 98)"/>
+<use xlink:href="#a" transform="translate(14 168)"/>
+<use xlink:href="#a" transform="translate(14 182)"/>
+<use xlink:href="#a" transform="translate(14 196)"/>
+<use xlink:href="#a" transform="translate(14 210)"/>
+<use xlink:href="#a" transform="translate(14 224)"/>
+<use xlink:href="#a" transform="translate(14 238)"/>
+<use xlink:href="#a" transform="translate(14 252)"/>
+<use xlink:href="#a" transform="translate(14 266)"/>
+<use xlink:href="#a" transform="translate(28)"/>
+<use xlink:href="#a" transform="translate(28 14)"/>
+<use xlink:href="#a" transform="translate(28 28)"/>
+<use xlink:href="#a" transform="translate(28 42)"/>
+<use xlink:href="#a" transform="translate(28 56)"/>
+<use xlink:href="#a" transform="translate(28 70)"/>
+<use xlink:href="#a" transform="translate(28 84)"/>
+<use xlink:href="#a" transform="translate(28 182)"/>
+<use xlink:href="#a" transform="translate(28 196)"/>
+<use xlink:href="#a" transform="translate(28 210)"/>
+<use xlink:href="#a" transform="translate(28 224)"/>
+<use xlink:href="#a" transform="translate(28 238)"/>
+<use xlink:href="#a" transform="translate(28 252)"/>
+<use xlink:href="#a" transform="translate(28 266)"/>
+<use xlink:href="#a" transform="translate(42)"/>
+<use xlink:href="#a" transform="translate(42 14)"/>
+<use xlink:href="#a" transform="translate(42 28)"/>
+<use xlink:href="#a" transform="translate(42 42)"/>
+<use xlink:href="#a" transform="translate(42 56)"/>
+<use xlink:href="#a" transform="translate(42 70)"/>
+<use xlink:href="#a" transform="translate(42 84)"/>
+<use xlink:href="#a" transform="translate(42 182)"/>
+<use xlink:href="#a" transform="translate(42 196)"/>
+<use xlink:href="#a" transform="translate(42 210)"/>
+<use xlink:href="#a" transform="translate(42 224)"/>
+<use xlink:href="#a" transform="translate(42 238)"/>
+<use xlink:href="#a" transform="translate(42 252)"/>
+<use xlink:href="#a" transform="translate(42 266)"/>
+<use xlink:href="#a" transform="translate(56)"/>
+<use xlink:href="#a" transform="translate(56 14)"/>
+<use xlink:href="#a" transform="translate(56 28)"/>
+<use xlink:href="#a" transform="translate(56 42)"/>
+<use xlink:href="#a" transform="translate(56 56)"/>
+<use xlink:href="#a" transform="translate(56 70)"/>
+<use xlink:href="#a" transform="translate(56 84)"/>
+<use xlink:href="#a" transform="translate(56 98)"/>
+<use xlink:href="#a" transform="translate(56 168)"/>
+<use xlink:href="#a" transform="translate(56 182)"/>
+<use xlink:href="#a" transform="translate(56 196)"/>
+<use xlink:href="#a" transform="translate(56 210)"/>
+<use xlink:href="#a" transform="translate(56 224)"/>
+<use xlink:href="#a" transform="translate(56 238)"/>
+<use xlink:href="#a" transform="translate(56 252)"/>
+<use xlink:href="#a" transform="translate(56 266)"/>
+<use xlink:href="#a" transform="translate(70)"/>
+<use xlink:href="#a" transform="translate(70 14)"/>
+<use xlink:href="#a" transform="translate(70 28)"/>
+<use xlink:href="#a" transform="translate(70 42)"/>
+<use xlink:href="#a" transform="translate(70 56)"/>
+<use xlink:href="#a" transform="translate(70 70)"/>
+<use xlink:href="#a" transform="translate(70 84)"/>
+<use xlink:href="#a" transform="translate(70 98)"/>
+<use xlink:href="#a" transform="translate(70 112)"/>
+<use xlink:href="#a" transform="translate(70 154)"/>
+<use xlink:href="#a" transform="translate(70 168)"/>
+<use xlink:href="#a" transform="translate(70 182)"/>
+<use xlink:href="#a" transform="translate(70 196)"/>
+<use xlink:href="#a" transform="translate(70 210)"/>
+<use xlink:href="#a" transform="translate(70 224)"/>
+<use xlink:href="#a" transform="translate(70 238)"/>
+<use xlink:href="#a" transform="translate(70 252)"/>
+<use xlink:href="#a" transform="translate(70 266)"/>
+<use xlink:href="#a" transform="translate(84 14)"/>
+<use xlink:href="#a" transform="translate(84 28)"/>
+<use xlink:href="#a" transform="translate(84 42)"/>
+<use xlink:href="#a" transform="translate(84 56)"/>
+<use xlink:href="#a" transform="translate(84 70)"/>
+<use xlink:href="#a" transform="translate(84 84)"/>
+<use xlink:href="#a" transform="translate(84 98)"/>
+<use xlink:href="#a" transform="translate(84 112)"/>
+<use xlink:href="#a" transform="translate(84 126)"/>
+<use xlink:href="#a" transform="translate(84 140)"/>
+<use xlink:href="#a" transform="translate(84 154)"/>
+<use xlink:href="#a" transform="translate(84 168)"/>
+<use xlink:href="#a" transform="translate(84 182)"/>
+<use xlink:href="#a" transform="translate(84 196)"/>
+<use xlink:href="#a" transform="translate(84 210)"/>
+<use xlink:href="#a" transform="translate(84 224)"/>
+<use xlink:href="#a" transform="translate(84 238)"/>
+<use xlink:href="#a" transform="translate(84 252)"/>
+<use xlink:href="#a" transform="translate(98 28)"/>
+<use xlink:href="#a" transform="translate(98 42)"/>
+<use xlink:href="#a" transform="translate(98 56)"/>
+<use xlink:href="#a" transform="translate(98 70)"/>
+<use xlink:href="#a" transform="translate(98 84)"/>
+<use xlink:href="#a" transform="translate(98 98)"/>
+<use xlink:href="#a" transform="translate(98 112)"/>
+<use xlink:href="#a" transform="translate(98 126)"/>
+<use xlink:href="#a" transform="translate(98 140)"/>
+<use xlink:href="#a" transform="translate(98 154)"/>
+<use xlink:href="#a" transform="translate(98 168)"/>
+<use xlink:href="#a" transform="translate(98 182)"/>
+<use xlink:href="#a" transform="translate(98 196)"/>
+<use xlink:href="#a" transform="translate(98 210)"/>
+<use xlink:href="#a" transform="translate(98 224)"/>
+<use xlink:href="#a" transform="translate(98 238)"/>
+<use xlink:href="#a" transform="translate(112 42)"/>
+<use xlink:href="#a" transform="translate(112 56)"/>
+<use xlink:href="#a" transform="translate(112 70)"/>
+<use xlink:href="#a" transform="translate(112 84)"/>
+<use xlink:href="#a" transform="translate(112 98)"/>
+<use xlink:href="#a" transform="translate(112 112)"/>
+<use xlink:href="#a" transform="translate(112 126)"/>
+<use xlink:href="#a" transform="translate(112 140)"/>
+<use xlink:href="#a" transform="translate(112 154)"/>
+<use xlink:href="#a" transform="translate(112 168)"/>
+<use xlink:href="#a" transform="translate(112 182)"/>
+<use xlink:href="#a" transform="translate(112 196)"/>
+<use xlink:href="#a" transform="translate(112 210)"/>
+<use xlink:href="#a" transform="translate(112 224)"/>
+<use xlink:href="#a" transform="translate(126 56)"/>
+<use xlink:href="#a" transform="translate(126 70)"/>
+<use xlink:href="#a" transform="translate(126 84)"/>
+<use xlink:href="#a" transform="translate(126 98)"/>
+<use xlink:href="#a" transform="translate(126 112)"/>
+<use xlink:href="#a" transform="translate(126 126)"/>
+<use xlink:href="#a" transform="translate(126 140)"/>
+<use xlink:href="#a" transform="translate(126 154)"/>
+<use xlink:href="#a" transform="translate(126 168)"/>
+<use xlink:href="#a" transform="translate(126 182)"/>
+<use xlink:href="#a" transform="translate(126 196)"/>
+<use xlink:href="#a" transform="translate(126 210)"/>
+<use xlink:href="#a" transform="translate(140 70)"/>
+<use xlink:href="#a" transform="translate(140 84)"/>
+<use xlink:href="#a" transform="translate(140 98)"/>
+<use xlink:href="#a" transform="translate(140 112)"/>
+<use xlink:href="#a" transform="translate(140 126)"/>
+<use xlink:href="#a" transform="translate(140 140)"/>
+<use xlink:href="#a" transform="translate(140 154)"/>
+<use xlink:href="#a" transform="translate(140 168)"/>
+<use xlink:href="#a" transform="translate(140 182)"/>
+<use xlink:href="#a" transform="translate(140 196)"/>
+<use xlink:href="#a" transform="translate(154 84)"/>
+<use xlink:href="#a" transform="translate(154 98)"/>
+<use xlink:href="#a" transform="translate(154 112)"/>
+<use xlink:href="#a" transform="translate(154 126)"/>
+<use xlink:href="#a" transform="translate(154 140)"/>
+<use xlink:href="#a" transform="translate(154 154)"/>
+<use xlink:href="#a" transform="translate(154 168)"/>
+<use xlink:href="#a" transform="translate(154 182)"/>
+<use xlink:href="#a" transform="translate(168 98)"/>
+<use xlink:href="#a" transform="translate(168 112)"/>
+<use xlink:href="#a" transform="translate(168 126)"/>
+<use xlink:href="#a" transform="translate(168 140)"/>
+<use xlink:href="#a" transform="translate(168 154)"/>
+<use xlink:href="#a" transform="translate(168 168)"/>
+<g id="b" transform="translate(126 98)">
+<rect height="14" width="14" fill="#9a9298"/>
+<rect height="12" width="12" y="1" x="1" fill="#696267"/>
+<path d="m0.5 13.5v-13h13l-0.5 0.5h-12v12z" fill="#5a5458"/>
+<path d="m0.5 13.5h13v-13l-0.5 0.5v12h-12z" fill="#797276"/>
+</g>
+<use xlink:href="#b" transform="translate(-28,28)"/>
+<use xlink:href="#b" transform="translate(-28,42)"/>
+<use xlink:href="#b" transform="translate(-14,14)"/>
+<use xlink:href="#b" transform="translate(-14,28)"/>
+<use xlink:href="#b" transform="translate(-14,42)"/>
+<use xlink:href="#b" transform="translate(-14,56)"/>
+<use xlink:href="#b" transform="translate(0,14)"/>
+<use xlink:href="#b" transform="translate(0,28)"/>
+<use xlink:href="#b" transform="translate(0,42)"/>
+<use xlink:href="#b" transform="translate(0,56)"/>
+<use xlink:href="#b" transform="translate(0,70)"/>
+<use xlink:href="#b" transform="translate(14)"/>
+<use xlink:href="#b" transform="translate(14,14)"/>
+<use xlink:href="#b" transform="translate(14,28)"/>
+<use xlink:href="#b" transform="translate(14,42)"/>
+<use xlink:href="#b" transform="translate(14,56)"/>
+<use xlink:href="#b" transform="translate(14,70)"/>
+<use xlink:href="#b" transform="translate(28,14)"/>
+<use xlink:href="#b" transform="translate(28,28)"/>
+<use xlink:href="#b" transform="translate(28,42)"/>
+<use xlink:href="#b" transform="translate(28,56)"/>
+<use xlink:href="#b" transform="translate(42,28)"/>
+<use xlink:href="#b" transform="translate(42,42)"/>
+</svg>
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 (file)
index 0000000..31c0c82
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="14" width="14" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect width="14" height="14" fill="#9a9298"/>
+<path d="m14 0h-14v14l0.7-0.7v-12.6h12.6z" fill="#767074"/>
+<path d="m14 0-0.7 0.7v12.6h-12.6l-0.7 0.7h14z" fill="#b2acb0"/>
+</svg>
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 (file)
index 0000000..7d5d4df
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="28" width="28" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect width="28" height="28" fill="#9a9298"/>
+<g id="a">
+<path d="m0 28v-21h7l-1 1h-5v19z" fill="#726c70"/>
+<path d="m0 28 1-1h5v-19l1-1v21z" fill="#b2acb0"/>
+</g>
+<use xlink:href="#a" transform="matrix(0,1,1,0,0,0)"/>
+</svg>
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 (file)
index 0000000..5450dc1
--- /dev/null
@@ -0,0 +1,394 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="320" width="320" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m0 160 80-160h160l80 160-80 160h-160z" fill="#9a9298"/>
+<g id="c">
+<path d="m70 20 1.732-1.155l8.268-16.845v-2z" fill="#767074"/>
+<path d="m70 20h20l-10-20v2l8.27 16.845h-16.538z" fill="#B2ACB0"/>
+</g>
+<g id="d">
+<path d="m100 0-1.732 1.155l-8.268 16.845v2z" fill="#B2ACB0"/>
+<path d="m100 0h-20l10 20v-2l-8.27-16.845h16.538z" fill="#767074"/>
+</g>
+<use xlink:href="#d" x="-80" y="160"/>
+<use xlink:href="#d" x="-70" y="180"/>
+<use xlink:href="#d" x="-60" y="120"/>
+<use xlink:href="#d" x="-70" y="140"/>
+<use xlink:href="#d" x="-60" y="160"/>
+<use xlink:href="#d" x="-50" y="180"/>
+<use xlink:href="#d" x="-60" y="200"/>
+<use xlink:href="#d" x="-50" y="220"/>
+<use xlink:href="#d" x="-40" y="80"/>
+<use xlink:href="#d" x="-50" y="100"/>
+<use xlink:href="#d" x="-40" y="120"/>
+<use xlink:href="#d" x="-50" y="140"/>
+<use xlink:href="#d" x="-40" y="160"/>
+<use xlink:href="#d" x="-30" y="180"/>
+<use xlink:href="#d" x="-40" y="200"/>
+<use xlink:href="#d" x="-30" y="220"/>
+<use xlink:href="#d" x="-40" y="240"/>
+<use xlink:href="#d" x="-30" y="260"/>
+<use xlink:href="#d" x="-20" y="40"/>
+<use xlink:href="#d" x="-30" y="60"/>
+<use xlink:href="#d" x="-20" y="80"/>
+<use xlink:href="#d" x="-30" y="100"/>
+<use xlink:href="#d" x="-20" y="120"/>
+<use xlink:href="#d" x="-30" y="140"/>
+<use xlink:href="#d" x="-20" y="160"/>
+<use xlink:href="#d" x="-10" y="180"/>
+<use xlink:href="#d" x="-20" y="200"/>
+<use xlink:href="#d" x="-10" y="220"/>
+<use xlink:href="#d" x="-20" y="240"/>
+<use xlink:href="#d" x="-10" y="260"/>
+<use xlink:href="#d" x="-20" y="280"/>
+<use xlink:href="#d" x="-10" y="300"/>
+<use xlink:href="#d" x="-10" y="20"/>
+<use xlink:href="#d" y="40"/>
+<use xlink:href="#d" x="-10" y="60"/>
+<use xlink:href="#d" y="80"/>
+<use xlink:href="#d" x="-10" y="100"/>
+<use xlink:href="#d" y="120"/>
+<use xlink:href="#d" x="-10" y="140"/>
+<use xlink:href="#d" y="160"/>
+<use xlink:href="#d" x="10" y="180"/>
+<use xlink:href="#d" y="200"/>
+<use xlink:href="#d" x="10" y="220"/>
+<use xlink:href="#d" y="240"/>
+<use xlink:href="#d" x="10" y="260"/>
+<use xlink:href="#d" y="280"/>
+<use xlink:href="#d" x="10" y="300"/>
+<use xlink:href="#d" x="20"/>
+<use xlink:href="#d" x="10" y="20"/>
+<use xlink:href="#d" x="20" y="40"/>
+<use xlink:href="#d" x="10" y="60"/>
+<use xlink:href="#d" x="20" y="80"/>
+<use xlink:href="#d" x="10" y="100"/>
+<use xlink:href="#d" x="20" y="120"/>
+<use xlink:href="#d" x="10" y="140"/>
+<use xlink:href="#d" x="20" y="160"/>
+<use xlink:href="#d" x="30" y="180"/>
+<use xlink:href="#d" x="20" y="200"/>
+<use xlink:href="#d" x="30" y="220"/>
+<use xlink:href="#d" x="20" y="240"/>
+<use xlink:href="#d" x="30" y="260"/>
+<use xlink:href="#d" x="20" y="280"/>
+<use xlink:href="#d" x="30" y="300"/>
+<use xlink:href="#d" x="40"/>
+<use xlink:href="#d" x="30" y="20"/>
+<use xlink:href="#d" x="40" y="40"/>
+<use xlink:href="#d" x="30" y="60"/>
+<use xlink:href="#d" x="40" y="80"/>
+<use xlink:href="#d" x="30" y="100"/>
+<use xlink:href="#d" x="40" y="120"/>
+<use xlink:href="#d" x="30" y="140"/>
+<use xlink:href="#d" x="40" y="160"/>
+<use xlink:href="#d" x="50" y="180"/>
+<use xlink:href="#d" x="40" y="200"/>
+<use xlink:href="#d" x="50" y="220"/>
+<use xlink:href="#d" x="40" y="240"/>
+<use xlink:href="#d" x="50" y="260"/>
+<use xlink:href="#d" x="40" y="280"/>
+<use xlink:href="#d" x="50" y="300"/>
+<use xlink:href="#d" x="60"/>
+<use xlink:href="#d" x="50" y="20"/>
+<use xlink:href="#d" x="60" y="40"/>
+<use xlink:href="#d" x="50" y="60"/>
+<use xlink:href="#d" x="60" y="80"/>
+<use xlink:href="#d" x="50" y="100"/>
+<use xlink:href="#d" x="60" y="120"/>
+<use xlink:href="#d" x="50" y="140"/>
+<use xlink:href="#d" x="60" y="160"/>
+<use xlink:href="#d" x="70" y="180"/>
+<use xlink:href="#d" x="60" y="200"/>
+<use xlink:href="#d" x="70" y="220"/>
+<use xlink:href="#d" x="60" y="240"/>
+<use xlink:href="#d" x="70" y="260"/>
+<use xlink:href="#d" x="60" y="280"/>
+<use xlink:href="#d" x="70" y="300"/>
+<use xlink:href="#d" x="80"/>
+<use xlink:href="#d" x="70" y="20"/>
+<use xlink:href="#d" x="80" y="40"/>
+<use xlink:href="#d" x="70" y="60"/>
+<use xlink:href="#d" x="80" y="80"/>
+<use xlink:href="#d" x="70" y="100"/>
+<use xlink:href="#d" x="80" y="120"/>
+<use xlink:href="#d" x="70" y="140"/>
+<use xlink:href="#d" x="80" y="160"/>
+<use xlink:href="#d" x="90" y="180"/>
+<use xlink:href="#d" x="80" y="200"/>
+<use xlink:href="#d" x="90" y="220"/>
+<use xlink:href="#d" x="80" y="240"/>
+<use xlink:href="#d" x="90" y="260"/>
+<use xlink:href="#d" x="80" y="280"/>
+<use xlink:href="#d" x="90" y="300"/>
+<use xlink:href="#d" x="100"/>
+<use xlink:href="#d" x="90" y="20"/>
+<use xlink:href="#d" x="100" y="40"/>
+<use xlink:href="#d" x="90" y="60"/>
+<use xlink:href="#d" x="100" y="80"/>
+<use xlink:href="#d" x="90" y="100"/>
+<use xlink:href="#d" x="100" y="120"/>
+<use xlink:href="#d" x="90" y="140"/>
+<use xlink:href="#d" x="100" y="160"/>
+<use xlink:href="#d" x="110" y="180"/>
+<use xlink:href="#d" x="100" y="200"/>
+<use xlink:href="#d" x="110" y="220"/>
+<use xlink:href="#d" x="100" y="240"/>
+<use xlink:href="#d" x="110" y="260"/>
+<use xlink:href="#d" x="100" y="280"/>
+<use xlink:href="#d" x="110" y="300"/>
+<use xlink:href="#d" x="120"/>
+<use xlink:href="#d" x="110" y="20"/>
+<use xlink:href="#d" x="120" y="40"/>
+<use xlink:href="#d" x="110" y="60"/>
+<use xlink:href="#d" x="120" y="80"/>
+<use xlink:href="#d" x="110" y="100"/>
+<use xlink:href="#d" x="120" y="120"/>
+<use xlink:href="#d" x="110" y="140"/>
+<use xlink:href="#d" x="120" y="160"/>
+<use xlink:href="#d" x="130" y="180"/>
+<use xlink:href="#d" x="120" y="200"/>
+<use xlink:href="#d" x="130" y="220"/>
+<use xlink:href="#d" x="120" y="240"/>
+<use xlink:href="#d" x="130" y="260"/>
+<use xlink:href="#d" x="120" y="280"/>
+<use xlink:href="#d" x="130" y="300"/>
+<use xlink:href="#d" x="140"/>
+<use xlink:href="#d" x="130" y="20"/>
+<use xlink:href="#d" x="140" y="40"/>
+<use xlink:href="#d" x="130" y="60"/>
+<use xlink:href="#d" x="140" y="80"/>
+<use xlink:href="#d" x="130" y="100"/>
+<use xlink:href="#d" x="140" y="120"/>
+<use xlink:href="#d" x="130" y="140"/>
+<use xlink:href="#d" x="140" y="160"/>
+<use xlink:href="#d" x="150" y="180"/>
+<use xlink:href="#d" x="140" y="200"/>
+<use xlink:href="#d" x="150" y="220"/>
+<use xlink:href="#d" x="140" y="240"/>
+<use xlink:href="#d" x="150" y="260"/>
+<use xlink:href="#d" x="140" y="280"/>
+<use xlink:href="#d" x="150" y="300"/>
+<use xlink:href="#d" x="150" y="20"/>
+<use xlink:href="#d" x="160" y="40"/>
+<use xlink:href="#d" x="150" y="60"/>
+<use xlink:href="#d" x="160" y="80"/>
+<use xlink:href="#d" x="150" y="100"/>
+<use xlink:href="#d" x="160" y="120"/>
+<use xlink:href="#d" x="150" y="140"/>
+<use xlink:href="#d" x="160" y="160"/>
+<use xlink:href="#d" x="170" y="180"/>
+<use xlink:href="#d" x="160" y="200"/>
+<use xlink:href="#d" x="170" y="220"/>
+<use xlink:href="#d" x="160" y="240"/>
+<use xlink:href="#d" x="170" y="260"/>
+<use xlink:href="#d" x="160" y="280"/>
+<use xlink:href="#d" x="170" y="60"/>
+<use xlink:href="#d" x="180" y="80"/>
+<use xlink:href="#d" x="170" y="100"/>
+<use xlink:href="#d" x="180" y="120"/>
+<use xlink:href="#d" x="170" y="140"/>
+<use xlink:href="#d" x="180" y="160"/>
+<use xlink:href="#d" x="190" y="180"/>
+<use xlink:href="#d" x="180" y="200"/>
+<use xlink:href="#d" x="190" y="220"/>
+<use xlink:href="#d" x="180" y="240"/>
+<use xlink:href="#d" x="190" y="100"/>
+<use xlink:href="#d" x="200" y="120"/>
+<use xlink:href="#d" x="190" y="140"/>
+<use xlink:href="#d" x="200" y="160"/>
+<use xlink:href="#d" x="210" y="180"/>
+<use xlink:href="#d" x="200" y="200"/>
+<use xlink:href="#d" x="210" y="140"/>
+<use xlink:href="#d" x="220" y="160"/>
+<use xlink:href="#c" x="-60" y="120"/>
+<use xlink:href="#c" x="-70" y="140"/>
+<use xlink:href="#c" x="-60" y="160"/>
+<use xlink:href="#c" x="-50" y="180"/>
+<use xlink:href="#c" x="-40" y="80"/>
+<use xlink:href="#c" x="-50" y="100"/>
+<use xlink:href="#c" x="-40" y="120"/>
+<use xlink:href="#c" x="-50" y="140"/>
+<use xlink:href="#c" x="-40" y="160"/>
+<use xlink:href="#c" x="-30" y="180"/>
+<use xlink:href="#c" x="-40" y="200"/>
+<use xlink:href="#c" x="-30" y="220"/>
+<use xlink:href="#c" x="-20" y="40"/>
+<use xlink:href="#c" x="-30" y="60"/>
+<use xlink:href="#c" x="-20" y="80"/>
+<use xlink:href="#c" x="-30" y="100"/>
+<use xlink:href="#c" x="-20" y="120"/>
+<use xlink:href="#c" x="-30" y="140"/>
+<use xlink:href="#c" x="-20" y="160"/>
+<use xlink:href="#c" x="-10" y="180"/>
+<use xlink:href="#c" x="-20" y="200"/>
+<use xlink:href="#c" x="-10" y="220"/>
+<use xlink:href="#c" x="-20" y="240"/>
+<use xlink:href="#c" x="-10" y="260"/>
+<use xlink:href="#c" x="-10" y="20"/>
+<use xlink:href="#c" y="40"/>
+<use xlink:href="#c" x="-10" y="60"/>
+<use xlink:href="#c" y="80"/>
+<use xlink:href="#c" x="-10" y="100"/>
+<use xlink:href="#c" y="120"/>
+<use xlink:href="#c" x="-10" y="140"/>
+<use xlink:href="#c" y="160"/>
+<use xlink:href="#c" x="10" y="180"/>
+<use xlink:href="#c" y="200"/>
+<use xlink:href="#c" x="10" y="220"/>
+<use xlink:href="#c" y="240"/>
+<use xlink:href="#c" x="10" y="260"/>
+<use xlink:href="#c" y="280"/>
+<use xlink:href="#c" x="10" y="300"/>
+<use xlink:href="#c" x="20"/>
+<use xlink:href="#c" x="10" y="20"/>
+<use xlink:href="#c" x="20" y="40"/>
+<use xlink:href="#c" x="10" y="60"/>
+<use xlink:href="#c" x="20" y="80"/>
+<use xlink:href="#c" x="10" y="100"/>
+<use xlink:href="#c" x="20" y="120"/>
+<use xlink:href="#c" x="10" y="140"/>
+<use xlink:href="#c" x="20" y="160"/>
+<use xlink:href="#c" x="30" y="180"/>
+<use xlink:href="#c" x="20" y="200"/>
+<use xlink:href="#c" x="30" y="220"/>
+<use xlink:href="#c" x="20" y="240"/>
+<use xlink:href="#c" x="30" y="260"/>
+<use xlink:href="#c" x="20" y="280"/>
+<use xlink:href="#c" x="30" y="300"/>
+<use xlink:href="#c" x="40"/>
+<use xlink:href="#c" x="30" y="20"/>
+<use xlink:href="#c" x="40" y="40"/>
+<use xlink:href="#c" x="30" y="60"/>
+<use xlink:href="#c" x="40" y="80"/>
+<use xlink:href="#c" x="30" y="100"/>
+<use xlink:href="#c" x="40" y="120"/>
+<use xlink:href="#c" x="30" y="140"/>
+<use xlink:href="#c" x="40" y="160"/>
+<use xlink:href="#c" x="50" y="180"/>
+<use xlink:href="#c" x="40" y="200"/>
+<use xlink:href="#c" x="50" y="220"/>
+<use xlink:href="#c" x="40" y="240"/>
+<use xlink:href="#c" x="50" y="260"/>
+<use xlink:href="#c" x="40" y="280"/>
+<use xlink:href="#c" x="50" y="300"/>
+<use xlink:href="#c" x="60"/>
+<use xlink:href="#c" x="50" y="20"/>
+<use xlink:href="#c" x="60" y="40"/>
+<use xlink:href="#c" x="50" y="60"/>
+<use xlink:href="#c" x="60" y="80"/>
+<use xlink:href="#c" x="50" y="100"/>
+<use xlink:href="#c" x="60" y="120"/>
+<use xlink:href="#c" x="50" y="140"/>
+<use xlink:href="#c" x="60" y="160"/>
+<use xlink:href="#c" x="70" y="180"/>
+<use xlink:href="#c" x="60" y="200"/>
+<use xlink:href="#c" x="70" y="220"/>
+<use xlink:href="#c" x="60" y="240"/>
+<use xlink:href="#c" x="70" y="260"/>
+<use xlink:href="#c" x="60" y="280"/>
+<use xlink:href="#c" x="70" y="300"/>
+<use xlink:href="#c" x="80"/>
+<use xlink:href="#c" x="70" y="20"/>
+<use xlink:href="#c" x="80" y="40"/>
+<use xlink:href="#c" x="70" y="60"/>
+<use xlink:href="#c" x="80" y="80"/>
+<use xlink:href="#c" x="70" y="100"/>
+<use xlink:href="#c" x="80" y="120"/>
+<use xlink:href="#c" x="70" y="140"/>
+<use xlink:href="#c" x="80" y="160"/>
+<use xlink:href="#c" x="90" y="180"/>
+<use xlink:href="#c" x="80" y="200"/>
+<use xlink:href="#c" x="90" y="220"/>
+<use xlink:href="#c" x="80" y="240"/>
+<use xlink:href="#c" x="90" y="260"/>
+<use xlink:href="#c" x="80" y="280"/>
+<use xlink:href="#c" x="90" y="300"/>
+<use xlink:href="#c" x="100"/>
+<use xlink:href="#c" x="90" y="20"/>
+<use xlink:href="#c" x="100" y="40"/>
+<use xlink:href="#c" x="90" y="60"/>
+<use xlink:href="#c" x="100" y="80"/>
+<use xlink:href="#c" x="90" y="100"/>
+<use xlink:href="#c" x="100" y="120"/>
+<use xlink:href="#c" x="90" y="140"/>
+<use xlink:href="#c" x="100" y="160"/>
+<use xlink:href="#c" x="110" y="180"/>
+<use xlink:href="#c" x="100" y="200"/>
+<use xlink:href="#c" x="110" y="220"/>
+<use xlink:href="#c" x="100" y="240"/>
+<use xlink:href="#c" x="110" y="260"/>
+<use xlink:href="#c" x="100" y="280"/>
+<use xlink:href="#c" x="110" y="300"/>
+<use xlink:href="#c" x="120"/>
+<use xlink:href="#c" x="110" y="20"/>
+<use xlink:href="#c" x="120" y="40"/>
+<use xlink:href="#c" x="110" y="60"/>
+<use xlink:href="#c" x="120" y="80"/>
+<use xlink:href="#c" x="110" y="100"/>
+<use xlink:href="#c" x="120" y="120"/>
+<use xlink:href="#c" x="110" y="140"/>
+<use xlink:href="#c" x="120" y="160"/>
+<use xlink:href="#c" x="130" y="180"/>
+<use xlink:href="#c" x="120" y="200"/>
+<use xlink:href="#c" x="130" y="220"/>
+<use xlink:href="#c" x="120" y="240"/>
+<use xlink:href="#c" x="130" y="260"/>
+<use xlink:href="#c" x="120" y="280"/>
+<use xlink:href="#c" x="130" y="300"/>
+<use xlink:href="#c" x="140"/>
+<use xlink:href="#c" x="130" y="20"/>
+<use xlink:href="#c" x="140" y="40"/>
+<use xlink:href="#c" x="130" y="60"/>
+<use xlink:href="#c" x="140" y="80"/>
+<use xlink:href="#c" x="130" y="100"/>
+<use xlink:href="#c" x="140" y="120"/>
+<use xlink:href="#c" x="130" y="140"/>
+<use xlink:href="#c" x="140" y="160"/>
+<use xlink:href="#c" x="150" y="180"/>
+<use xlink:href="#c" x="140" y="200"/>
+<use xlink:href="#c" x="150" y="220"/>
+<use xlink:href="#c" x="140" y="240"/>
+<use xlink:href="#c" x="150" y="260"/>
+<use xlink:href="#c" x="140" y="280"/>
+<use xlink:href="#c" x="150" y="300"/>
+<use xlink:href="#c" x="160"/>
+<use xlink:href="#c" x="150" y="20"/>
+<use xlink:href="#c" x="160" y="40"/>
+<use xlink:href="#c" x="150" y="60"/>
+<use xlink:href="#c" x="160" y="80"/>
+<use xlink:href="#c" x="150" y="100"/>
+<use xlink:href="#c" x="160" y="120"/>
+<use xlink:href="#c" x="150" y="140"/>
+<use xlink:href="#c" x="160" y="160"/>
+<use xlink:href="#c" x="170" y="180"/>
+<use xlink:href="#c" x="160" y="200"/>
+<use xlink:href="#c" x="170" y="220"/>
+<use xlink:href="#c" x="160" y="240"/>
+<use xlink:href="#c" x="170" y="260"/>
+<use xlink:href="#c" x="160" y="280"/>
+<use xlink:href="#c" x="170" y="20"/>
+<use xlink:href="#c" x="180" y="40"/>
+<use xlink:href="#c" x="170" y="60"/>
+<use xlink:href="#c" x="180" y="80"/>
+<use xlink:href="#c" x="170" y="100"/>
+<use xlink:href="#c" x="180" y="120"/>
+<use xlink:href="#c" x="170" y="140"/>
+<use xlink:href="#c" x="180" y="160"/>
+<use xlink:href="#c" x="190" y="180"/>
+<use xlink:href="#c" x="180" y="200"/>
+<use xlink:href="#c" x="190" y="220"/>
+<use xlink:href="#c" x="180" y="240"/>
+<use xlink:href="#c" x="190" y="60"/>
+<use xlink:href="#c" x="200" y="80"/>
+<use xlink:href="#c" x="190" y="100"/>
+<use xlink:href="#c" x="200" y="120"/>
+<use xlink:href="#c" x="190" y="140"/>
+<use xlink:href="#c" x="200" y="160"/>
+<use xlink:href="#c" x="210" y="180"/>
+<use xlink:href="#c" x="200" y="200"/>
+<use xlink:href="#c" x="210" y="100"/>
+<use xlink:href="#c" x="220" y="120"/>
+<use xlink:href="#c" x="210" y="140"/>
+<use xlink:href="#c" x="220" y="160"/>
+<use xlink:href="#c" x="230" y="140"/>
+</svg>
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 (file)
index 0000000..d7964e1
--- /dev/null
@@ -0,0 +1,496 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="360" width="360" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m90 0-90 180 90 180h180l90-180-90-180z" fill="#9a9298"/>
+<g id="c">
+<path d="m80 20 1.732-1.155l8.268-16.845v-2z" fill="#767074"/>
+<path d="m80 20h20l-10-20v2l8.27 16.845h-16.538z" fill="#B2ACB0"/>
+</g>
+<g id="d">
+<path d="m110 0-1.732 1.155l-8.268 16.845v2z" fill="#B2ACB0"/>
+<path d="m110 0h-20l10 20v-2l-8.27-16.845h16.538z" fill="#767074"/>
+</g>
+<use xlink:href="#d" x="-90" y="180"/>
+<use xlink:href="#d" x="-80" y="200"/>
+<use xlink:href="#d" x="-70" y="140"/>
+<use xlink:href="#d" x="-80" y="160"/>
+<use xlink:href="#d" x="-70" y="180"/>
+<use xlink:href="#d" x="-60" y="200"/>
+<use xlink:href="#d" x="-70" y="220"/>
+<use xlink:href="#d" x="-60" y="240"/>
+<use xlink:href="#d" x="-50" y="100"/>
+<use xlink:href="#d" x="-60" y="120"/>
+<use xlink:href="#d" x="-50" y="140"/>
+<use xlink:href="#d" x="-60" y="160"/>
+<use xlink:href="#d" x="-50" y="180"/>
+<use xlink:href="#d" x="-40" y="200"/>
+<use xlink:href="#d" x="-50" y="220"/>
+<use xlink:href="#d" x="-40" y="240"/>
+<use xlink:href="#d" x="-50" y="260"/>
+<use xlink:href="#d" x="-40" y="280"/>
+<use xlink:href="#d" x="-30" y="60"/>
+<use xlink:href="#d" x="-40" y="80"/>
+<use xlink:href="#d" x="-30" y="100"/>
+<use xlink:href="#d" x="-40" y="120"/>
+<use xlink:href="#d" x="-30" y="140"/>
+<use xlink:href="#d" x="-40" y="160"/>
+<use xlink:href="#d" x="-30" y="180"/>
+<use xlink:href="#d" x="-20" y="200"/>
+<use xlink:href="#d" x="-30" y="220"/>
+<use xlink:href="#d" x="-20" y="240"/>
+<use xlink:href="#d" x="-30" y="260"/>
+<use xlink:href="#d" x="-20" y="280"/>
+<use xlink:href="#d" x="-30" y="300"/>
+<use xlink:href="#d" x="-20" y="320"/>
+<use xlink:href="#d" x="-10" y="20"/>
+<use xlink:href="#d" x="-20" y="40"/>
+<use xlink:href="#d" x="-10" y="60"/>
+<use xlink:href="#d" x="-20" y="80"/>
+<use xlink:href="#d" x="-10" y="100"/>
+<use xlink:href="#d" x="-20" y="120"/>
+<use xlink:href="#d" x="-10" y="140"/>
+<use xlink:href="#d" x="-20" y="160"/>
+<use xlink:href="#d" x="-10" y="180"/>
+<use xlink:href="#d" y="200"/>
+<use xlink:href="#d" x="-10" y="220"/>
+<use xlink:href="#d" y="240"/>
+<use xlink:href="#d" x="-10" y="260"/>
+<use xlink:href="#d" y="280"/>
+<use xlink:href="#d" x="-10" y="300"/>
+<use xlink:href="#d" y="320"/>
+<use xlink:href="#d" x="-10" y="340"/>
+<use xlink:href="#d" x="10" y="20"/>
+<use xlink:href="#d" y="40"/>
+<use xlink:href="#d" x="10" y="60"/>
+<use xlink:href="#d" y="80"/>
+<use xlink:href="#d" x="10" y="100"/>
+<use xlink:href="#d" y="120"/>
+<use xlink:href="#d" x="10" y="140"/>
+<use xlink:href="#d" y="160"/>
+<use xlink:href="#d" x="10" y="180"/>
+<use xlink:href="#d" x="20" y="200"/>
+<use xlink:href="#d" x="10" y="220"/>
+<use xlink:href="#d" x="20" y="240"/>
+<use xlink:href="#d" x="10" y="260"/>
+<use xlink:href="#d" x="20" y="280"/>
+<use xlink:href="#d" x="10" y="300"/>
+<use xlink:href="#d" x="20" y="320"/>
+<use xlink:href="#d" x="10" y="340"/>
+<use xlink:href="#d" x="20"/>
+<use xlink:href="#d" x="30" y="20"/>
+<use xlink:href="#d" x="20" y="40"/>
+<use xlink:href="#d" x="30" y="60"/>
+<use xlink:href="#d" x="20" y="80"/>
+<use xlink:href="#d" x="30" y="100"/>
+<use xlink:href="#d" x="20" y="120"/>
+<use xlink:href="#d" x="30" y="140"/>
+<use xlink:href="#d" x="20" y="160"/>
+<use xlink:href="#d" x="30" y="180"/>
+<use xlink:href="#d" x="40" y="200"/>
+<use xlink:href="#d" x="30" y="220"/>
+<use xlink:href="#d" x="40" y="240"/>
+<use xlink:href="#d" x="30" y="260"/>
+<use xlink:href="#d" x="40" y="280"/>
+<use xlink:href="#d" x="30" y="300"/>
+<use xlink:href="#d" x="40" y="320"/>
+<use xlink:href="#d" x="30" y="340"/>
+<use xlink:href="#d" x="40"/>
+<use xlink:href="#d" x="50" y="20"/>
+<use xlink:href="#d" x="40" y="40"/>
+<use xlink:href="#d" x="50" y="60"/>
+<use xlink:href="#d" x="40" y="80"/>
+<use xlink:href="#d" x="50" y="100"/>
+<use xlink:href="#d" x="40" y="120"/>
+<use xlink:href="#d" x="50" y="140"/>
+<use xlink:href="#d" x="40" y="160"/>
+<use xlink:href="#d" x="50" y="180"/>
+<use xlink:href="#d" x="60" y="200"/>
+<use xlink:href="#d" x="50" y="220"/>
+<use xlink:href="#d" x="60" y="240"/>
+<use xlink:href="#d" x="50" y="260"/>
+<use xlink:href="#d" x="60" y="280"/>
+<use xlink:href="#d" x="50" y="300"/>
+<use xlink:href="#d" x="60" y="320"/>
+<use xlink:href="#d" x="50" y="340"/>
+<use xlink:href="#d" x="60"/>
+<use xlink:href="#d" x="70" y="20"/>
+<use xlink:href="#d" x="60" y="40"/>
+<use xlink:href="#d" x="70" y="60"/>
+<use xlink:href="#d" x="60" y="80"/>
+<use xlink:href="#d" x="70" y="100"/>
+<use xlink:href="#d" x="60" y="120"/>
+<use xlink:href="#d" x="70" y="140"/>
+<use xlink:href="#d" x="60" y="160"/>
+<use xlink:href="#d" x="70" y="180"/>
+<use xlink:href="#d" x="80" y="200"/>
+<use xlink:href="#d" x="70" y="220"/>
+<use xlink:href="#d" x="80" y="240"/>
+<use xlink:href="#d" x="70" y="260"/>
+<use xlink:href="#d" x="80" y="280"/>
+<use xlink:href="#d" x="70" y="300"/>
+<use xlink:href="#d" x="80" y="320"/>
+<use xlink:href="#d" x="70" y="340"/>
+<use xlink:href="#d" x="80"/>
+<use xlink:href="#d" x="90" y="20"/>
+<use xlink:href="#d" x="80" y="40"/>
+<use xlink:href="#d" x="90" y="60"/>
+<use xlink:href="#d" x="80" y="80"/>
+<use xlink:href="#d" x="90" y="100"/>
+<use xlink:href="#d" x="80" y="120"/>
+<use xlink:href="#d" x="90" y="140"/>
+<use xlink:href="#d" x="80" y="160"/>
+<use xlink:href="#d" x="90" y="180"/>
+<use xlink:href="#d" x="100" y="200"/>
+<use xlink:href="#d" x="90" y="220"/>
+<use xlink:href="#d" x="100" y="240"/>
+<use xlink:href="#d" x="90" y="260"/>
+<use xlink:href="#d" x="100" y="280"/>
+<use xlink:href="#d" x="90" y="300"/>
+<use xlink:href="#d" x="100" y="320"/>
+<use xlink:href="#d" x="90" y="340"/>
+<use xlink:href="#d" x="100"/>
+<use xlink:href="#d" x="110" y="20"/>
+<use xlink:href="#d" x="100" y="40"/>
+<use xlink:href="#d" x="110" y="60"/>
+<use xlink:href="#d" x="100" y="80"/>
+<use xlink:href="#d" x="110" y="100"/>
+<use xlink:href="#d" x="100" y="120"/>
+<use xlink:href="#d" x="110" y="140"/>
+<use xlink:href="#d" x="100" y="160"/>
+<use xlink:href="#d" x="110" y="180"/>
+<use xlink:href="#d" x="120" y="200"/>
+<use xlink:href="#d" x="110" y="220"/>
+<use xlink:href="#d" x="120" y="240"/>
+<use xlink:href="#d" x="110" y="260"/>
+<use xlink:href="#d" x="120" y="280"/>
+<use xlink:href="#d" x="110" y="300"/>
+<use xlink:href="#d" x="120" y="320"/>
+<use xlink:href="#d" x="110" y="340"/>
+<use xlink:href="#d" x="120"/>
+<use xlink:href="#d" x="130" y="20"/>
+<use xlink:href="#d" x="120" y="40"/>
+<use xlink:href="#d" x="130" y="60"/>
+<use xlink:href="#d" x="120" y="80"/>
+<use xlink:href="#d" x="130" y="100"/>
+<use xlink:href="#d" x="120" y="120"/>
+<use xlink:href="#d" x="130" y="140"/>
+<use xlink:href="#d" x="120" y="160"/>
+<use xlink:href="#d" x="130" y="180"/>
+<use xlink:href="#d" x="140" y="200"/>
+<use xlink:href="#d" x="130" y="220"/>
+<use xlink:href="#d" x="140" y="240"/>
+<use xlink:href="#d" x="130" y="260"/>
+<use xlink:href="#d" x="140" y="280"/>
+<use xlink:href="#d" x="130" y="300"/>
+<use xlink:href="#d" x="140" y="320"/>
+<use xlink:href="#d" x="130" y="340"/>
+<use xlink:href="#d" x="140"/>
+<use xlink:href="#d" x="150" y="20"/>
+<use xlink:href="#d" x="140" y="40"/>
+<use xlink:href="#d" x="150" y="60"/>
+<use xlink:href="#d" x="140" y="80"/>
+<use xlink:href="#d" x="150" y="100"/>
+<use xlink:href="#d" x="140" y="120"/>
+<use xlink:href="#d" x="150" y="140"/>
+<use xlink:href="#d" x="140" y="160"/>
+<use xlink:href="#d" x="150" y="180"/>
+<use xlink:href="#d" x="160" y="200"/>
+<use xlink:href="#d" x="150" y="220"/>
+<use xlink:href="#d" x="160" y="240"/>
+<use xlink:href="#d" x="150" y="260"/>
+<use xlink:href="#d" x="160" y="280"/>
+<use xlink:href="#d" x="150" y="300"/>
+<use xlink:href="#d" x="160" y="320"/>
+<use xlink:href="#d" x="150" y="340"/>
+<use xlink:href="#d" x="160"/>
+<use xlink:href="#d" x="170" y="20"/>
+<use xlink:href="#d" x="160" y="40"/>
+<use xlink:href="#d" x="170" y="60"/>
+<use xlink:href="#d" x="160" y="80"/>
+<use xlink:href="#d" x="170" y="100"/>
+<use xlink:href="#d" x="160" y="120"/>
+<use xlink:href="#d" x="170" y="140"/>
+<use xlink:href="#d" x="160" y="160"/>
+<use xlink:href="#d" x="170" y="180"/>
+<use xlink:href="#d" x="180" y="200"/>
+<use xlink:href="#d" x="170" y="220"/>
+<use xlink:href="#d" x="180" y="240"/>
+<use xlink:href="#d" x="170" y="260"/>
+<use xlink:href="#d" x="180" y="280"/>
+<use xlink:href="#d" x="170" y="300"/>
+<use xlink:href="#d" x="180" y="320"/>
+<use xlink:href="#d" x="170" y="340"/>
+<use xlink:href="#d" x="180" y="40"/>
+<use xlink:href="#d" x="190" y="60"/>
+<use xlink:href="#d" x="180" y="80"/>
+<use xlink:href="#d" x="190" y="100"/>
+<use xlink:href="#d" x="180" y="120"/>
+<use xlink:href="#d" x="190" y="140"/>
+<use xlink:href="#d" x="180" y="160"/>
+<use xlink:href="#d" x="190" y="180"/>
+<use xlink:href="#d" x="200" y="200"/>
+<use xlink:href="#d" x="190" y="220"/>
+<use xlink:href="#d" x="200" y="240"/>
+<use xlink:href="#d" x="190" y="260"/>
+<use xlink:href="#d" x="200" y="280"/>
+<use xlink:href="#d" x="190" y="300"/>
+<use xlink:href="#d" x="200" y="80"/>
+<use xlink:href="#d" x="210" y="100"/>
+<use xlink:href="#d" x="200" y="120"/>
+<use xlink:href="#d" x="210" y="140"/>
+<use xlink:href="#d" x="200" y="160"/>
+<use xlink:href="#d" x="210" y="180"/>
+<use xlink:href="#d" x="220" y="200"/>
+<use xlink:href="#d" x="210" y="220"/>
+<use xlink:href="#d" x="220" y="240"/>
+<use xlink:href="#d" x="210" y="260"/>
+<use xlink:href="#d" x="220" y="120"/>
+<use xlink:href="#d" x="230" y="140"/>
+<use xlink:href="#d" x="220" y="160"/>
+<use xlink:href="#d" x="230" y="180"/>
+<use xlink:href="#d" x="240" y="200"/>
+<use xlink:href="#d" x="230" y="220"/>
+<use xlink:href="#d" x="240" y="160"/>
+<use xlink:href="#d" x="250" y="180"/>
+<use xlink:href="#c" x="-70" y="140"/>
+<use xlink:href="#c" x="-80" y="160"/>
+<use xlink:href="#c" x="-70" y="180"/>
+<use xlink:href="#c" x="-60" y="200"/>
+<use xlink:href="#c" x="-50" y="100"/>
+<use xlink:href="#c" x="-60" y="120"/>
+<use xlink:href="#c" x="-50" y="140"/>
+<use xlink:href="#c" x="-60" y="160"/>
+<use xlink:href="#c" x="-50" y="180"/>
+<use xlink:href="#c" x="-40" y="200"/>
+<use xlink:href="#c" x="-50" y="220"/>
+<use xlink:href="#c" x="-40" y="240"/>
+<use xlink:href="#c" x="-30" y="60"/>
+<use xlink:href="#c" x="-40" y="80"/>
+<use xlink:href="#c" x="-30" y="100"/>
+<use xlink:href="#c" x="-40" y="120"/>
+<use xlink:href="#c" x="-30" y="140"/>
+<use xlink:href="#c" x="-40" y="160"/>
+<use xlink:href="#c" x="-30" y="180"/>
+<use xlink:href="#c" x="-20" y="200"/>
+<use xlink:href="#c" x="-30" y="220"/>
+<use xlink:href="#c" x="-20" y="240"/>
+<use xlink:href="#c" x="-30" y="260"/>
+<use xlink:href="#c" x="-20" y="280"/>
+<use xlink:href="#c" x="-10" y="20"/>
+<use xlink:href="#c" x="-20" y="40"/>
+<use xlink:href="#c" x="-10" y="60"/>
+<use xlink:href="#c" x="-20" y="80"/>
+<use xlink:href="#c" x="-10" y="100"/>
+<use xlink:href="#c" x="-20" y="120"/>
+<use xlink:href="#c" x="-10" y="140"/>
+<use xlink:href="#c" x="-20" y="160"/>
+<use xlink:href="#c" x="-10" y="180"/>
+<use xlink:href="#c" y="200"/>
+<use xlink:href="#c" x="-10" y="220"/>
+<use xlink:href="#c" y="240"/>
+<use xlink:href="#c" x="-10" y="260"/>
+<use xlink:href="#c" y="280"/>
+<use xlink:href="#c" x="-10" y="300"/>
+<use xlink:href="#c" y="320"/>
+<use xlink:href="#c" x="10" y="20"/>
+<use xlink:href="#c" y="40"/>
+<use xlink:href="#c" x="10" y="60"/>
+<use xlink:href="#c" y="80"/>
+<use xlink:href="#c" x="10" y="100"/>
+<use xlink:href="#c" y="120"/>
+<use xlink:href="#c" x="10" y="140"/>
+<use xlink:href="#c" y="160"/>
+<use xlink:href="#c" x="10" y="180"/>
+<use xlink:href="#c" x="20" y="200"/>
+<use xlink:href="#c" x="10" y="220"/>
+<use xlink:href="#c" x="20" y="240"/>
+<use xlink:href="#c" x="10" y="260"/>
+<use xlink:href="#c" x="20" y="280"/>
+<use xlink:href="#c" x="10" y="300"/>
+<use xlink:href="#c" x="20" y="320"/>
+<use xlink:href="#c" x="10" y="340"/>
+<use xlink:href="#c" x="20"/>
+<use xlink:href="#c" x="30" y="20"/>
+<use xlink:href="#c" x="20" y="40"/>
+<use xlink:href="#c" x="30" y="60"/>
+<use xlink:href="#c" x="20" y="80"/>
+<use xlink:href="#c" x="30" y="100"/>
+<use xlink:href="#c" x="20" y="120"/>
+<use xlink:href="#c" x="30" y="140"/>
+<use xlink:href="#c" x="20" y="160"/>
+<use xlink:href="#c" x="30" y="180"/>
+<use xlink:href="#c" x="40" y="200"/>
+<use xlink:href="#c" x="30" y="220"/>
+<use xlink:href="#c" x="40" y="240"/>
+<use xlink:href="#c" x="30" y="260"/>
+<use xlink:href="#c" x="40" y="280"/>
+<use xlink:href="#c" x="30" y="300"/>
+<use xlink:href="#c" x="40" y="320"/>
+<use xlink:href="#c" x="30" y="340"/>
+<use xlink:href="#c" x="40"/>
+<use xlink:href="#c" x="50" y="20"/>
+<use xlink:href="#c" x="40" y="40"/>
+<use xlink:href="#c" x="50" y="60"/>
+<use xlink:href="#c" x="40" y="80"/>
+<use xlink:href="#c" x="50" y="100"/>
+<use xlink:href="#c" x="40" y="120"/>
+<use xlink:href="#c" x="50" y="140"/>
+<use xlink:href="#c" x="40" y="160"/>
+<use xlink:href="#c" x="50" y="180"/>
+<use xlink:href="#c" x="60" y="200"/>
+<use xlink:href="#c" x="50" y="220"/>
+<use xlink:href="#c" x="60" y="240"/>
+<use xlink:href="#c" x="50" y="260"/>
+<use xlink:href="#c" x="60" y="280"/>
+<use xlink:href="#c" x="50" y="300"/>
+<use xlink:href="#c" x="60" y="320"/>
+<use xlink:href="#c" x="50" y="340"/>
+<use xlink:href="#c" x="60"/>
+<use xlink:href="#c" x="70" y="20"/>
+<use xlink:href="#c" x="60" y="40"/>
+<use xlink:href="#c" x="70" y="60"/>
+<use xlink:href="#c" x="60" y="80"/>
+<use xlink:href="#c" x="70" y="100"/>
+<use xlink:href="#c" x="60" y="120"/>
+<use xlink:href="#c" x="70" y="140"/>
+<use xlink:href="#c" x="60" y="160"/>
+<use xlink:href="#c" x="70" y="180"/>
+<use xlink:href="#c" x="80" y="200"/>
+<use xlink:href="#c" x="70" y="220"/>
+<use xlink:href="#c" x="80" y="240"/>
+<use xlink:href="#c" x="70" y="260"/>
+<use xlink:href="#c" x="80" y="280"/>
+<use xlink:href="#c" x="70" y="300"/>
+<use xlink:href="#c" x="80" y="320"/>
+<use xlink:href="#c" x="70" y="340"/>
+<use xlink:href="#c" x="80"/>
+<use xlink:href="#c" x="90" y="20"/>
+<use xlink:href="#c" x="80" y="40"/>
+<use xlink:href="#c" x="90" y="60"/>
+<use xlink:href="#c" x="80" y="80"/>
+<use xlink:href="#c" x="90" y="100"/>
+<use xlink:href="#c" x="80" y="120"/>
+<use xlink:href="#c" x="90" y="140"/>
+<use xlink:href="#c" x="80" y="160"/>
+<use xlink:href="#c" x="90" y="180"/>
+<use xlink:href="#c" x="100" y="200"/>
+<use xlink:href="#c" x="90" y="220"/>
+<use xlink:href="#c" x="100" y="240"/>
+<use xlink:href="#c" x="90" y="260"/>
+<use xlink:href="#c" x="100" y="280"/>
+<use xlink:href="#c" x="90" y="300"/>
+<use xlink:href="#c" x="100" y="320"/>
+<use xlink:href="#c" x="90" y="340"/>
+<use xlink:href="#c" x="100"/>
+<use xlink:href="#c" x="110" y="20"/>
+<use xlink:href="#c" x="100" y="40"/>
+<use xlink:href="#c" x="110" y="60"/>
+<use xlink:href="#c" x="100" y="80"/>
+<use xlink:href="#c" x="110" y="100"/>
+<use xlink:href="#c" x="100" y="120"/>
+<use xlink:href="#c" x="110" y="140"/>
+<use xlink:href="#c" x="100" y="160"/>
+<use xlink:href="#c" x="110" y="180"/>
+<use xlink:href="#c" x="120" y="200"/>
+<use xlink:href="#c" x="110" y="220"/>
+<use xlink:href="#c" x="120" y="240"/>
+<use xlink:href="#c" x="110" y="260"/>
+<use xlink:href="#c" x="120" y="280"/>
+<use xlink:href="#c" x="110" y="300"/>
+<use xlink:href="#c" x="120" y="320"/>
+<use xlink:href="#c" x="110" y="340"/>
+<use xlink:href="#c" x="120"/>
+<use xlink:href="#c" x="130" y="20"/>
+<use xlink:href="#c" x="120" y="40"/>
+<use xlink:href="#c" x="130" y="60"/>
+<use xlink:href="#c" x="120" y="80"/>
+<use xlink:href="#c" x="130" y="100"/>
+<use xlink:href="#c" x="120" y="120"/>
+<use xlink:href="#c" x="130" y="140"/>
+<use xlink:href="#c" x="120" y="160"/>
+<use xlink:href="#c" x="130" y="180"/>
+<use xlink:href="#c" x="140" y="200"/>
+<use xlink:href="#c" x="130" y="220"/>
+<use xlink:href="#c" x="140" y="240"/>
+<use xlink:href="#c" x="130" y="260"/>
+<use xlink:href="#c" x="140" y="280"/>
+<use xlink:href="#c" x="130" y="300"/>
+<use xlink:href="#c" x="140" y="320"/>
+<use xlink:href="#c" x="130" y="340"/>
+<use xlink:href="#c" x="140"/>
+<use xlink:href="#c" x="150" y="20"/>
+<use xlink:href="#c" x="140" y="40"/>
+<use xlink:href="#c" x="150" y="60"/>
+<use xlink:href="#c" x="140" y="80"/>
+<use xlink:href="#c" x="150" y="100"/>
+<use xlink:href="#c" x="140" y="120"/>
+<use xlink:href="#c" x="150" y="140"/>
+<use xlink:href="#c" x="140" y="160"/>
+<use xlink:href="#c" x="150" y="180"/>
+<use xlink:href="#c" x="160" y="200"/>
+<use xlink:href="#c" x="150" y="220"/>
+<use xlink:href="#c" x="160" y="240"/>
+<use xlink:href="#c" x="150" y="260"/>
+<use xlink:href="#c" x="160" y="280"/>
+<use xlink:href="#c" x="150" y="300"/>
+<use xlink:href="#c" x="160" y="320"/>
+<use xlink:href="#c" x="150" y="340"/>
+<use xlink:href="#c" x="160"/>
+<use xlink:href="#c" x="170" y="20"/>
+<use xlink:href="#c" x="160" y="40"/>
+<use xlink:href="#c" x="170" y="60"/>
+<use xlink:href="#c" x="160" y="80"/>
+<use xlink:href="#c" x="170" y="100"/>
+<use xlink:href="#c" x="160" y="120"/>
+<use xlink:href="#c" x="170" y="140"/>
+<use xlink:href="#c" x="160" y="160"/>
+<use xlink:href="#c" x="170" y="180"/>
+<use xlink:href="#c" x="180" y="200"/>
+<use xlink:href="#c" x="170" y="220"/>
+<use xlink:href="#c" x="180" y="240"/>
+<use xlink:href="#c" x="170" y="260"/>
+<use xlink:href="#c" x="180" y="280"/>
+<use xlink:href="#c" x="170" y="300"/>
+<use xlink:href="#c" x="180" y="320"/>
+<use xlink:href="#c" x="170" y="340"/>
+<use xlink:href="#c" x="180"/>
+<use xlink:href="#c" x="190" y="20"/>
+<use xlink:href="#c" x="180" y="40"/>
+<use xlink:href="#c" x="190" y="60"/>
+<use xlink:href="#c" x="180" y="80"/>
+<use xlink:href="#c" x="190" y="100"/>
+<use xlink:href="#c" x="180" y="120"/>
+<use xlink:href="#c" x="190" y="140"/>
+<use xlink:href="#c" x="180" y="160"/>
+<use xlink:href="#c" x="190" y="180"/>
+<use xlink:href="#c" x="200" y="200"/>
+<use xlink:href="#c" x="190" y="220"/>
+<use xlink:href="#c" x="200" y="240"/>
+<use xlink:href="#c" x="190" y="260"/>
+<use xlink:href="#c" x="200" y="280"/>
+<use xlink:href="#c" x="190" y="300"/>
+<use xlink:href="#c" x="200" y="40"/>
+<use xlink:href="#c" x="210" y="60"/>
+<use xlink:href="#c" x="200" y="80"/>
+<use xlink:href="#c" x="210" y="100"/>
+<use xlink:href="#c" x="200" y="120"/>
+<use xlink:href="#c" x="210" y="140"/>
+<use xlink:href="#c" x="200" y="160"/>
+<use xlink:href="#c" x="210" y="180"/>
+<use xlink:href="#c" x="220" y="200"/>
+<use xlink:href="#c" x="210" y="220"/>
+<use xlink:href="#c" x="220" y="240"/>
+<use xlink:href="#c" x="210" y="260"/>
+<use xlink:href="#c" x="220" y="80"/>
+<use xlink:href="#c" x="230" y="100"/>
+<use xlink:href="#c" x="220" y="120"/>
+<use xlink:href="#c" x="230" y="140"/>
+<use xlink:href="#c" x="220" y="160"/>
+<use xlink:href="#c" x="230" y="180"/>
+<use xlink:href="#c" x="240" y="200"/>
+<use xlink:href="#c" x="230" y="220"/>
+<use xlink:href="#c" x="240" y="120"/>
+<use xlink:href="#c" x="250" y="140"/>
+<use xlink:href="#c" x="240" y="160"/>
+<use xlink:href="#c" x="250" y="180"/>
+<use xlink:href="#c" x="260" y="160"/>
+</svg>
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 (file)
index 0000000..b55e32e
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path fill="#0073cf" d="m0 0v20h20v-20l-20-6.2e-7zm3.5 3.5h13v13h-13v-13z"/>
+<path fill="#004881" d="m0 20h20v-20l-1 1v18h-18z"/>
+<path d="m0 20 1-1v-18h18l1-1h-20z" fill="#0e94ff"/>
+</svg>
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 (file)
index 0000000..dae34d9
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path fill="#00c000" d="m0 0v20h20v-20l-20-6.2e-7zm3.5 3.5h13v13h-13v-13z"/>
+<path fill="#007800" d="m0 20h20v-20l-1 1v18h-18z"/>
+<path d="m0 20 1-1v-18h18l1-1h-20z" fill="#00fa00"/>
+</svg>
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 (file)
index 0000000..b73327b
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path fill="#e63e2c" d="m0 0v20h20v-20l-20-6.2e-7zm3.5 3.5h13v13h-13v-13z"/>
+<path fill="#90261b" d="m0 20h20v-20l-1 1v18h-18z"/>
+<path d="m0 20 1-1v-18h18l1-1h-20z" fill="#fa6253"/>
+</svg>
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 (file)
index 0000000..948a459
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path fill="#ebcd23" d="m0 0v20h20v-20l-20-6.2e-7zm3.5 3.5h13v13h-13v-13z"/>
+<path fill="#938015" d="m0 20h20v-20l-1 1v18h-18z"/>
+<path d="m0 20 1-1v-18h18l1-1h-20z" fill="#ffe658"/>
+</svg>
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 (file)
index 0000000..339e9d0
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="7" width="7" fill="#0073cf"/>
+</svg>
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 (file)
index 0000000..02f2de2
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="7" width="7" fill="#00c000"/>
+</svg>
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 (file)
index 0000000..6f7b77c
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="7" width="7" fill="#e63e2c"/>
+</svg>
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 (file)
index 0000000..52f3c58
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="7" width="7" fill="#ebcd23"/>
+</svg>
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 (file)
index 0000000..74dea2c
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m7 7v-5l-5 5z" fill="#0073cf"/>
+</svg>
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 (file)
index 0000000..6b88d65
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m7 7v-5l-5 5z" fill="#00c000"/>
+</svg>
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 (file)
index 0000000..fdd2958
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m7 7v-5l-5 5z" fill="#e63e2c"/>
+</svg>
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 (file)
index 0000000..d13407c
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m7 7v-5l-5 5z" fill="#ebcd23"/>
+</svg>
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 (file)
index 0000000..05fce46
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="4" width="7" y="1.5" fill="#0073cf"/>
+</svg>
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 (file)
index 0000000..321ef53
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="4" width="7" y="1.5" fill="#00c000"/>
+</svg>
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 (file)
index 0000000..78f4a3a
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="4" width="7" y="1.5" fill="#e63e2c"/>
+</svg>
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 (file)
index 0000000..eb9a97c
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="4" width="7" y="1.5" fill="#ebcd23"/>
+</svg>
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 (file)
index 0000000..3468515
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="5" width="7" y="2" fill="#0073cf"/>
+</svg>
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 (file)
index 0000000..1f36933
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="5" width="7" y="2" fill="#00c000"/>
+</svg>
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 (file)
index 0000000..b9a1367
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="5" width="7" y="2" fill="#e63e2c"/>
+</svg>
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 (file)
index 0000000..cf431aa
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="5" width="7" y="2" fill="#ebcd23"/>
+</svg>
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 (file)
index 0000000..a4f9eb1
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="21" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="7" width="21" fill="#0073cf"/>
+<path fill="#004881" d="m0 7h21v-7l-1 1v5h-19z"/>
+<path fill="#0e94ff" d="m0 7 1-1v-5h19l1-1h-21z"/>
+</svg>
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 (file)
index 0000000..c8a0160
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="21" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect width="21" height="7" fill="#00c000"/>
+<path d="m0 7h21v-7l-1 1v5h-19z" fill="#007800"/>
+<path d="m0 7 1-1v-5h19l1-1h-21z" fill="#00fa00"/>
+</svg>
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 (file)
index 0000000..102bee5
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="21" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="7" width="21" fill="#e63e2c"/>
+<path d="m0 7h21v-7l-1 1v5h-19z" fill="#90261b"/>
+<path d="m0 7 1-1v-5h19l1-1h-21z" fill="#fa6253"/>
+</svg>
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 (file)
index 0000000..548d2d8
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="21" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="7" width="21" fill="#ebcd23"/>
+<path d="m0 7h21v-7l-1 1v5h-19z" fill="#938015"/>
+<path d="m0 7 1-1v-5h19l1-1h-21z" fill="#ffe658"/>
+</svg>
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 (file)
index 0000000..05928bb
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="200" width="200" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m200 100a100 100 0 0 1 -200 0 100 100 0 1 1 200 0z" fill="#70716d"/>
+<path d="m160 100a60 60 0 0 1 -120 0 60 60 0 1 1 120 0z" fill="#fafafa"/>
+<g fill="#555753">
+<g id="c">
+<path fill="#fafafa" d="m40 100a20 20 0 0 1 -40 0 20 20 0 1 1 40 0z"/>
+<path d="m20 87.5c6.9035 0 12.5 5.5964 12.5 12.5h-5.3571c0-3.9449-3.198-7.1429-7.1428-7.1429-3.9449 0-7.1429 3.198-7.1429 7.1429 0 2.0384 0.86101 3.8449 2.2322 5.1339l3.3482-3.3482v10.714h-10.714l3.5714-3.5714c-2.389-2.3238-3.7946-5.5105-3.7946-8.9286 0-6.9036 5.5964-12.5 12.5-12.5z"/>
+</g>
+<g id="d">
+<path fill="#fafafa" d="m120 180a20 20 0 0 1 -40 0 20 20 0 1 1 40 0z"/>
+<path d="m95 170-10 10 10 10v-6.9375h10v7l10-10-10-9.9375v6.9375h-10z"/>
+</g>
+<use xlink:href="#d" transform="matrix(0 1 -1 0 360 0)"/>
+<use xlink:href="#c" transform="matrix(-1 0 0 1 120 -80)"/>
+</g>
+</svg>
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 (file)
index 0000000..187e09e
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="200" width="200" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m200 100a100 100 0 0 1 -200 0 100 100 0 1 1 200 0z" fill="#70716d"/>
+<path d="m160 100a60 60 0 1 1 -120 0 60 60 0 1 1 120 0z" fill="#9c9c90"/>
+<g id="c">
+<path d="m40 100a20 20 0 0 1 -40 0 20 20 0 1 1 40 0z" fill="#fafafa"/>
+<path d="m20 87.5c6.9035 0 12.5 5.5964 12.5 12.5h-5.3571c0-3.9449-3.198-7.1428-7.1428-7.1428-3.9449 0-7.1429 3.198-7.1429 7.1428 0 2.0384 0.86101 3.8449 2.2322 5.1339l3.3482-3.3482v10.714h-10.714l3.5714-3.5714c-2.389-2.3238-3.7946-5.5105-3.7946-8.9286 0-6.9036 5.5964-12.5 12.5-12.5z" fill="#555753"/>
+</g>
+<g id="d">
+<path d="m120 180a20 20 0 0 1 -40 0 20 20 0 1 1 40 0z" fill="#fafafa"/>
+<path d="m95 170-10 10 10 9.9375v-6.9375h10v7l10-10-10-9.9375v6.9375h-10z" fill="#555753"/>
+</g>
+<use xlink:href="#c" transform="matrix(-1 0 0 1 120 -80)"/>
+<use xlink:href="#d" transform="matrix(0 1 -1 0 360 0)"/>
+</svg>
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 (file)
index 0000000..63ed8aa
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="20" width="20" fill="#0073cf"/>
+<path fill="#004881" d="m0 20h20v-20l-1 1v18h-18z"/>
+<path fill="#0e94ff" d="m0 20 1-1v-18h18l1-1h-20z"/>
+</svg>
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 (file)
index 0000000..916e69c
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="20" width="20" fill="#00c000"/>
+<path d="m0 20h20v-20l-1 1v18h-18z" fill="#007800"/>
+<path d="m0 20 1-1v-18h18l1-1h-20z" fill="#00fa00"/>
+</svg>
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 (file)
index 0000000..0e58a50
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="20" width="20" fill="#e63e2c"/>
+<path fill="#90261b" d="m0 20h20v-20l-1 1v18h-18z"/>
+<path fill="#fa6253" d="m0 20 1-1v-18h18l1-1h-20z"/>
+</svg>
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 (file)
index 0000000..1989427
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="20" width="20" fill="#ebcd23"/>
+<path fill="#938015" d="m0 20h20v-20l-1 1v18h-18z"/>
+<path fill="#ffe658" d="m0 20 1-1v-18h18l1-1h-20z"/>
+</svg>
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 (file)
index 0000000..7a82407
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m10 0 10 20h-20z" fill="#0073cf"/>
+<path d="m0 20 1.6-1 8.4-16.8v-2.2z" fill="#0e94ff"/>
+<path d="m0 20 1.6-1h16.8l1.6 1z" fill="#004881"/>
+<path d="m20 20-10-20v2.2l8.4 16.8z" fill="#0059a0"/>
+</svg>
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 (file)
index 0000000..d93a7e0
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m10 20 10-20h-20z" fill="#0073cf"/>
+<path fill="#0080e6" d="m0 0 1.6 0.96 8.4 16.8v2.2z"/>
+<path fill="#0e94ff" d="m0 0 1.6 0.96h16.8l1.6-0.96z"/>
+<path fill="#004881" d="m20 0-10 20v-2.2l8.4-16.8z"/>
+</svg>
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 (file)
index 0000000..d848f64
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m10 20 10-20h-20z" fill="#00c000"/>
+<path fill="#00d200" d="m0 0 1.6 0.96 8.4 16.8v2.2z"/>
+<path fill="#00fa00" d="m0 0 1.6 0.96h16.8l1.6-0.96z"/>
+<path fill="#007800" d="m20 0-10 20v-2.2l8.4-16.8z"/>
+</svg>
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 (file)
index 0000000..1b5f55b
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m10 20 10-20h-20z" fill="#e63e2c"/>
+<path fill="#f93e2b" d="m0 0 1.6 0.96 8.4 16.8v2.2z"/>
+<path fill="#fa6253" d="m0 0 1.6 0.96h16.8l1.6-0.96z"/>
+<path fill="#90261b" d="m20 0-10 20v-2.2l8.4-16.8z"/>
+</svg>
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 (file)
index 0000000..403f542
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m10 20 10-20h-20z" fill="#ebcd23"/>
+<path fill="#ffe133" d="m0 0 1.6 0.96 8.4 16.8v2.2z"/>
+<path fill="#ffe658" d="m0 0 1.6 0.96h16.8l1.6-0.96z"/>
+<path fill="#938015" d="m20 0-10 20v-2.2l8.4-16.8z"/>
+</svg>
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 (file)
index 0000000..198b7bb
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m10 0 10 20h-20z" fill="#00c000"/>
+<path fill="#00fa00" d="m0 20 1.6-1 8.4-16.8v-2.2z"/>
+<path fill="#007800" d="m0 20 1.6-1h16.8l1.6 1z"/>
+<path fill="#009700" d="m20 20-10-20v2.2l8.4 16.8z"/>
+</svg>
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 (file)
index 0000000..1afa8da
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m10 0 10 20h-20z" fill="#e63e2c"/>
+<path fill="#fa6253" d="m0 20 1.6-1 8.4-16.8v-2.2z"/>
+<path fill="#90261b" d="m0 20 1.6-1h16.8l1.6 1z"/>
+<path fill="#bb3223" d="m20 20-10-20v2.2l8.4 16.8z"/>
+</svg>
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 (file)
index 0000000..bb2c535
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m10 0 10 20h-20z" fill="#ebcd23"/>
+<path fill="#ffe658" d="m0 20 1.6-1 8.4-16.8v-2.2z"/>
+<path fill="#938015" d="m0 20 1.6-1h16.8l1.6 1z"/>
+<path fill="#b09919" d="m20 20-10-20v2.2l8.4 16.8z"/>
+</svg>
diff --git a/src/pentobi_qml/qml/themes/theme_dark.qrc b/src/pentobi_qml/qml/themes/theme_dark.qrc
new file mode 100644 (file)
index 0000000..08609ea
--- /dev/null
@@ -0,0 +1,12 @@
+<RCC>
+<qresource prefix="/qml/themes">
+<file>dark/board-callisto.svg</file>
+<file>dark/board-callisto-2.svg</file>
+<file>dark/board-callisto-3.svg</file>
+<file>dark/board-tile-classic.svg</file>
+<file>dark/board-tile-nexos.svg</file>
+<file>dark/board-trigon.svg</file>
+<file>dark/board-trigon-3.svg</file>
+<file>dark/Theme.qml</file>
+</qresource>
+</RCC>
diff --git a/src/pentobi_qml/qml/themes/theme_light.qrc b/src/pentobi_qml/qml/themes/theme_light.qrc
new file mode 100644 (file)
index 0000000..f4706d8
--- /dev/null
@@ -0,0 +1,12 @@
+<RCC>
+<qresource prefix="/qml/themes">
+<file>light/board-callisto.svg</file>
+<file>light/board-callisto-2.svg</file>
+<file>light/board-callisto-3.svg</file>
+<file>light/board-tile-classic.svg</file>
+<file>light/board-tile-nexos.svg</file>
+<file>light/board-trigon.svg</file>
+<file>light/board-trigon-3.svg</file>
+<file>light/Theme.qml</file>
+</qresource>
+</RCC>
diff --git a/src/pentobi_qml/qml/themes/theme_shared.qrc b/src/pentobi_qml/qml/themes/theme_shared.qrc
new file mode 100644 (file)
index 0000000..63f183b
--- /dev/null
@@ -0,0 +1,42 @@
+<RCC>
+<qresource prefix="/qml/themes">
+<file>light/frame-blue.svg</file>
+<file>light/frame-green.svg</file>
+<file>light/frame-red.svg</file>
+<file>light/frame-yellow.svg</file>
+<file>light/junction-all-blue.svg</file>
+<file>light/junction-all-green.svg</file>
+<file>light/junction-all-red.svg</file>
+<file>light/junction-all-yellow.svg</file>
+<file>light/junction-rect-blue.svg</file>
+<file>light/junction-rect-green.svg</file>
+<file>light/junction-rect-red.svg</file>
+<file>light/junction-rect-yellow.svg</file>
+<file>light/junction-straight-blue.svg</file>
+<file>light/junction-straight-green.svg</file>
+<file>light/junction-straight-red.svg</file>
+<file>light/junction-straight-yellow.svg</file>
+<file>light/junction-t-blue.svg</file>
+<file>light/junction-t-green.svg</file>
+<file>light/junction-t-red.svg</file>
+<file>light/junction-t-yellow.svg</file>
+<file>light/linesegment-blue.svg</file>
+<file>light/linesegment-green.svg</file>
+<file>light/linesegment-red.svg</file>
+<file>light/linesegment-yellow.svg</file>
+<file>light/piece-manipulator-legal.svg</file>
+<file>light/piece-manipulator.svg</file>
+<file>light/square-blue.svg</file>
+<file>light/square-green.svg</file>
+<file>light/square-red.svg</file>
+<file>light/square-yellow.svg</file>
+<file>light/triangle-blue.svg</file>
+<file>light/triangle-down-blue.svg</file>
+<file>light/triangle-down-green.svg</file>
+<file>light/triangle-down-red.svg</file>
+<file>light/triangle-down-yellow.svg</file>
+<file>light/triangle-green.svg</file>
+<file>light/triangle-red.svg</file>
+<file>light/triangle-yellow.svg</file>
+</qresource>
+</RCC>
diff --git a/src/pentobi_qml/resources.qrc b/src/pentobi_qml/resources.qrc
new file mode 100644 (file)
index 0000000..db9c32c
--- /dev/null
@@ -0,0 +1,40 @@
+<RCC>
+    <qresource prefix="/">
+        <file>qml/AndroidToolBar.qml</file>
+        <file>qml/AndroidToolButton.qml</file>
+        <file>qml/Board.qml</file>
+        <file>qml/Button.qml</file>
+        <file>qml/ComputerColorDialog.qml</file>
+        <file>qml/GameDisplay.qml</file>
+        <file>qml/LineSegment.qml</file>
+        <file>qml/Main.qml</file>
+        <file>qml/MenuComputer.qml</file>
+        <file>qml/MenuEdit.qml</file>
+        <file>qml/MenuGame.qml</file>
+        <file>qml/MenuGo.qml</file>
+        <file>qml/MenuHelp.qml</file>
+        <file>qml/MenuItemGameVariant.qml</file>
+        <file>qml/MenuItemLevel.qml</file>
+        <file>qml/MenuView.qml</file>
+        <file>qml/NavigationPanel.qml</file>
+        <file>qml/OpenDialog.qml</file>
+        <file>qml/PieceCallisto.qml</file>
+        <file>qml/PieceClassic.qml</file>
+        <file>qml/PieceFlipAnimation.qml</file>
+        <file>qml/PieceList.qml</file>
+        <file>qml/PieceManipulator.qml</file>
+        <file>qml/PieceNexos.qml</file>
+        <file>qml/PieceRotationAnimation.qml</file>
+        <file>qml/PieceSelector.qml</file>
+        <file>qml/PieceTrigon.qml</file>
+        <file>qml/SaveDialog.qml</file>
+        <file>qml/ScoreDisplay.qml</file>
+        <file>qml/ScoreElement.qml</file>
+        <file>qml/ScoreElement2.qml</file>
+        <file>qml/Square.qml</file>
+        <file>qml/ToolBar.qml</file>
+        <file>qml/Triangle.qml</file>
+        <file>qml/Main.js</file>
+        <file>qml/GameDisplay.js</file>
+    </qresource>
+</RCC>
diff --git a/src/pentobi_qml/translations.qrc b/src/pentobi_qml/translations.qrc
new file mode 100644 (file)
index 0000000..9cc6b55
--- /dev/null
@@ -0,0 +1,6 @@
+<RCC>
+    <qresource prefix="/">
+        <file>qml/i18n/qml_de.qm</file>
+        <file>qml/i18n/replace_qtbase_de.qm</file>
+    </qresource>
+</RCC>
diff --git a/src/pentobi_thumbnailer/CMakeLists.txt b/src/pentobi_thumbnailer/CMakeLists.txt
new file mode 100644 (file)
index 0000000..43339b0
--- /dev/null
@@ -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 (file)
index 0000000..c4f6e72
--- /dev/null
@@ -0,0 +1,55 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi_thumbnailer/Main.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include <iostream>
+#include <QCommandLineParser>
+#include <QCoreApplication>
+#include <QImage>
+#include <QImageWriter>
+#include <QString>
+#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>.",
+                    "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 (file)
index 0000000..4c1c974
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "Analyze.h"
+
+#include <fstream>
+#include <map>
+#include <regex>
+#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<unsigned, Statistics<>> stat_result_player;
+    map<float, unsigned> 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 (file)
index 0000000..e1dc754
--- /dev/null
@@ -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 <string>
+
+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 (file)
index 0000000..a4dc5ec
--- /dev/null
@@ -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 (file)
index 0000000..5ef5c81
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "FdStream.h"
+
+#include <cstring>
+#include <unistd.h>
+
+//-----------------------------------------------------------------------------
+
+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<char>(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 (file)
index 0000000..25ea211
--- /dev/null
@@ -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 <iostream>
+#include <vector>
+
+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<char_type> 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 (file)
index 0000000..2873fc6
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "GtpConnection.h"
+
+#include <cstdlib>
+#include <cstring>
+#include <vector>
+#include <unistd.h>
+#include "FdStream.h"
+#include "libboardgame_util/Log.h"
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+void terminate_child(const string& message)
+{
+    LIBBOARDGAME_LOG(message);
+    exit(1);
+}
+
+vector<string> split_args(string s)
+{
+    vector<string> 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<string> 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 (file)
index 0000000..3c21150
--- /dev/null
@@ -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 <iosfwd>
+#include <memory>
+#include <string>
+
+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<istream> m_in;
+
+    unique_ptr<ostream> m_out;
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // TWOGTP_GTP_CONNECTION_H
diff --git a/src/twogtp/Main.cpp b/src/twogtp/Main.cpp
new file mode 100644 (file)
index 0000000..ea90942
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include <atomic>
+#include <thread>
+#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<int> result(0);
+    try
+    {
+        vector<string> 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<unsigned>("nugames", 1);
+        auto nu_threads = opt.get<unsigned>("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<shared_ptr<TwoGtp>> 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<TwoGtp>(black, white, variant,
+                                                 nu_games, output, quiet,
+                                                 log_prefix, fast_open));
+        }
+        vector<thread> 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 (file)
index 0000000..e56d16a
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "Output.h"
+
+#include <cstdio>
+#include <fstream>
+#include <iomanip>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/file.h>
+#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<bool, Board::max_game_moves>& is_real_move)
+{
+    lock_guard<mutex> 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<mutex> 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<mutex> 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 (file)
index 0000000..7dc2c7f
--- /dev/null
@@ -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 <string>
+#include <map>
+#include <mutex>
+#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<bool, Board::max_game_moves>& 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<unsigned, string> 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 (file)
index 0000000..0898fd6
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "OutputTree.h"
+
+#include <fstream>
+#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<unsigned, 2> count;
+    array<double, 2> avg_result;
+    array<unsigned, 2> 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<double>::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<ColorMove, Board::max_game_moves>& s1,
+                      ArrayList<ColorMove, Board::max_game_moves>& 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<unsigned, 2> count;
+    array<double, 2> avg_result;
+    array<unsigned, 2> 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<bool, Board::max_game_moves>& is_real_move)
+{
+    if (bd.has_setup())
+        throw runtime_error("OutputTree: setup not supported");
+
+    // Find the canonical representation
+    ArrayList<ColorMove, Board::max_game_moves> sequence;
+    for (auto& transform : m_transforms)
+    {
+        ArrayList<ColorMove, Board::max_game_moves> 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<double> distribution(0, 1);
+    if (distribution(m_random) < 1.0 / sum)
+    {
+        play_real = true;
+        return;
+    }
+    unsigned random = static_cast<unsigned>(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 (file)
index 0000000..9211f8d
--- /dev/null
@@ -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 <random>
+#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<bool, Board::max_game_moves>& is_real_move);
+
+private:
+    typedef libboardgame_base::PointTransform<Point> PointTransform;
+
+    PentobiTree m_tree;
+
+    vector<unique_ptr<PointTransform>> m_transforms;
+
+    vector<unique_ptr<PointTransform>> 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 (file)
index 0000000..ced86db
--- /dev/null
@@ -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 <config.h>
+#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<ScoreType, Color::range> points;
+        for (Color::IntType i = 0; i < m_bd.get_nu_colors(); ++i)
+            points[i] = m_bd.get_points(Color(i));
+        array<float, Color::range> 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<bool, Board::max_game_moves> 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<char>(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 (file)
index 0000000..59116f9
--- /dev/null
@@ -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 <array>
+#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<string, Color::range> 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 (file)
index 0000000..88fb251
--- /dev/null
@@ -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 (file)
index 0000000..8ef1b1d
--- /dev/null
@@ -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 (file)
index 0000000..255d89a
--- /dev/null
@@ -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 <config.h>
+#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<Point> 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<unsigned>::digits > 32)
+        return;
+    Marker m;
+    m.setup_for_overflow_test(numeric_limits<unsigned>::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 (file)
index 0000000..2731b1e
--- /dev/null
@@ -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 <config.h>
+#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<Point> 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<Point> transform;
+        LIBBOARDGAME_CHECK(transform.get_transformed(p, geo) == p);
+    }
+    {
+        libboardgame_base::PointTransfRot180<Point> transform;
+        LIBBOARDGAME_CHECK(transform.get_transformed(p, geo)
+                           == geo.get_point(7, 6));
+    }
+    {
+        libboardgame_base::PointTransfRot270Refl<Point> 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 (file)
index 0000000..2ec5031
--- /dev/null
@@ -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 <config.h>
+#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 (file)
index 0000000..56a4a73
--- /dev/null
@@ -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 <config.h>
+#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<Point> Geometry;
+typedef libboardgame_base::RectGeometry<Point> RectGeometry;
+typedef libboardgame_base::ArrayList<Point, Point::max_onboard> 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 (file)
index 0000000..e348653
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include <sstream>
+#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 (file)
index 0000000..a42cae9
--- /dev/null
@@ -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 <config.h>
+#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<bool>(0));
+    }
+    {
+        CmdLine line("command 1");
+        Arguments args(line);
+        LIBBOARDGAME_CHECK(args.parse<bool>(0));
+    }
+    {
+        CmdLine line("command 2");
+        Arguments args(line);
+        LIBBOARDGAME_CHECK_THROW(args.parse<bool>(0), Failure);
+    }
+    {
+        CmdLine line("command arg1");
+        Arguments args(line);
+        LIBBOARDGAME_CHECK_THROW(args.parse<bool>(0), Failure);
+    }
+    {
+        CmdLine line("command");
+        Arguments args(line);
+        LIBBOARDGAME_CHECK_THROW(args.parse<bool>(0), Failure);
+    }
+}
+
+LIBBOARDGAME_TEST_CASE(gtp_arguments_float)
+{
+    CmdLine line("command abc 5.5");
+    Arguments args(line);
+    LIBBOARDGAME_CHECK_THROW(args.parse<float>(0), Failure);
+    LIBBOARDGAME_CHECK_CLOSE(5.5f, args.parse<float>(1), 1e-4);
+}
+
+LIBBOARDGAME_TEST_CASE(gtp_arguments_int)
+{
+    CmdLine line("command 5 arg");
+    Arguments args(line);
+    LIBBOARDGAME_CHECK_EQUAL(5, args.parse<int>(0));
+    LIBBOARDGAME_CHECK_THROW(args.parse<int>(1), Failure);
+}
+
+LIBBOARDGAME_TEST_CASE(gtp_arguments_min_int)
+{
+    CmdLine line("command 5");
+    Arguments args(line);
+    LIBBOARDGAME_CHECK_EQUAL(5, args.parse_min<int>(0, 3));
+    LIBBOARDGAME_CHECK_THROW(args.parse_min<int>(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<int>(0, 3, 10));
+    LIBBOARDGAME_CHECK_THROW(args.parse_min_max<int>(0, 0, 4), Failure);
+    LIBBOARDGAME_CHECK_THROW(args.parse_min_max<int>(0, 10, 20), Failure);
+}
+
+LIBBOARDGAME_TEST_CASE(gtp_arguments_single_int)
+{
+    {
+        CmdLine line("command 5");
+        Arguments args(line);
+        LIBBOARDGAME_CHECK_EQUAL(5, args.parse<int>());
+    }
+    {
+        CmdLine line("command 5 10");
+        Arguments args(line);
+        LIBBOARDGAME_CHECK_THROW(args.parse<int>(), 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 (file)
index 0000000..ecd1ca1
--- /dev/null
@@ -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 (file)
index 0000000..8ef988d
--- /dev/null
@@ -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 <config.h>
+#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 (file)
index 0000000..a501e0c
--- /dev/null
@@ -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 <config.h>
+#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 (file)
index 0000000..d025021
--- /dev/null
@@ -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 <config.h>
+#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 (file)
index 0000000..83ca0a6
--- /dev/null
@@ -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 (file)
index 0000000..e09bffe
--- /dev/null
@@ -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 <config.h>
+#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<int, float, true> 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<int, float, true> 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 (file)
index 0000000..b50e7b8
--- /dev/null
@@ -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 (file)
index 0000000..7acd4ca
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include <memory>
+#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<SgfNode> 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<SgfNode> 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 (file)
index 0000000..6421fb8
--- /dev/null
@@ -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 <config.h>
+#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<SgfNode> root(new SgfNode);
+    auto& child = root->create_new_child();
+    vector<const SgfNode*> 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 (file)
index 0000000..6db65c1
--- /dev/null
@@ -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 <config.h>
+#endif
+
+#include "libboardgame_sgf/TreeReader.h"
+
+#include <sstream>
+#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 (file)
index 0000000..05f00c1
--- /dev/null
@@ -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 <config.h>
+#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<int, 10> 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<int, 10> l(5);
+    LIBBOARDGAME_CHECK_EQUAL(1u, l.size());
+    LIBBOARDGAME_CHECK_EQUAL(5, l[0]);
+}
+
+LIBBOARDGAME_TEST_CASE(util_array_list_equals)
+{
+    ArrayList<int, 10> l1{ 1, 2, 3 };
+    ArrayList<int, 10> l2{ 1, 2, 3 };
+    LIBBOARDGAME_CHECK(l1 == l2);
+    l2.push_back(4);
+    LIBBOARDGAME_CHECK(! (l1 == l2));
+    l2 = ArrayList<int, 10>({ 2, 1, 3 });
+    LIBBOARDGAME_CHECK(! (l1 == l2));
+}
+
+LIBBOARDGAME_TEST_CASE(util_array_list_pop_back)
+{
+    ArrayList<int, 10> l(5);
+    int i = l.pop_back();
+    LIBBOARDGAME_CHECK_EQUAL(5, i);
+    LIBBOARDGAME_CHECK(l.empty());
+}
+
+LIBBOARDGAME_TEST_CASE(util_array_list_remove)
+{
+    ArrayList<int, 10> 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 (file)
index 0000000..c157257
--- /dev/null
@@ -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 (file)
index 0000000..842e9ea
--- /dev/null
@@ -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 <config.h>
+#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<string> 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<int>(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<string> specs = { "first:" };
+    const char* argv[] =
+        { nullptr, "--first", "firstval", "--", "--arg1" };
+    int argc = static_cast<int>(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<string> specs = { "first:" };
+    const char* argv[] = { nullptr, "--first" };
+    int argc = static_cast<int>(sizeof(argv) / sizeof(argv[0]));
+    LIBBOARDGAME_CHECK_THROW(Options opt(argc, argv, specs), runtime_error);
+}
+
+LIBBOARDGAME_TEST_CASE(libboardgame_util_options_nospace)
+{
+    vector<string> specs = { "first|a:", "second|b:" };
+    const char* argv[] = { nullptr, "-abc" };
+    int argc = static_cast<int>(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<string> specs = { "first|a", "second|b:" };
+    const char* argv[] = { nullptr, "-ab", "c" };
+    int argc = static_cast<int>(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<string> specs = { "first:", "second:" };
+    const char* argv[] = { nullptr, "--first", "10", "--second", "foo" };
+    int argc = static_cast<int>(sizeof(argv) / sizeof(argv[0]));
+    Options opt(argc, argv, specs);
+    LIBBOARDGAME_CHECK_EQUAL(opt.get<int>("first"), 10);
+    LIBBOARDGAME_CHECK_THROW(opt.get<int>("second"), runtime_error);
+}
+
+//-----------------------------------------------------------------------------
diff --git a/src/unittest/libboardgame_util/StatisticsTest.cpp b/src/unittest/libboardgame_util/StatisticsTest.cpp
new file mode 100644 (file)
index 0000000..0084a73
--- /dev/null
@@ -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 <config.h>
+#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<double> 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 (file)
index 0000000..0f10f5f
--- /dev/null
@@ -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 <config.h>
+#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<string> 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<string> v = split("", ',');
+        LIBBOARDGAME_CHECK_EQUAL(v.size(), 0u);
+    }
+    {
+        vector<string> v = split("a,", ',');
+        LIBBOARDGAME_CHECK_EQUAL(v.size(), 2u);
+        LIBBOARDGAME_CHECK_EQUAL(v[0], "a");
+        LIBBOARDGAME_CHECK_EQUAL(v[1], "");
+    }
+    {
+        vector<string> v = split(",a", ',');
+        LIBBOARDGAME_CHECK_EQUAL(v.size(), 2u);
+        LIBBOARDGAME_CHECK_EQUAL(v[0], "");
+        LIBBOARDGAME_CHECK_EQUAL(v[1], "a");
+    }
+    {
+        vector<string> 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 (file)
index 0000000..e8ed58d
--- /dev/null
@@ -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 <config.h>
+#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 (file)
index 0000000..a5b78b0
--- /dev/null
@@ -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 <config.h>
+#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<Board> 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<Board> bd(new Board(Variant::classic));
+    unique_ptr<MoveList> moves(new MoveList);
+    unique_ptr<MoveMarker> 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<Board> 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 (file)
index 0000000..f20d7db
--- /dev/null
@@ -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 <config.h>
+#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<SgfNode> root = reader.get_tree_transfer_ownership();
+    PentobiTree tree(root);
+    unique_ptr<Board> 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<SgfNode> root = reader.get_tree_transfer_ownership();
+    PentobiTree tree(root);
+    unique_ptr<Board> 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<SgfNode> root = reader.get_tree_transfer_ownership();
+    PentobiTree tree(root);
+    unique_ptr<Board> 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<SgfNode> root = reader.get_tree_transfer_ownership();
+    PentobiTree tree(root);
+    unique_ptr<Board> 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 (file)
index 0000000..1930c00
--- /dev/null
@@ -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 (file)
index 0000000..e9db78d
--- /dev/null
@@ -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 <config.h>
+#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<SgfNode> 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 (file)
index 0000000..1a2270a
--- /dev/null
@@ -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 <config.h>
+#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<SgfNode> 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<SgfNode> 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<SgfNode> 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 (file)
index 0000000..3879222
--- /dev/null
@@ -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 (file)
index 0000000..ab4333a
--- /dev/null
@@ -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 <config.h>
+#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<SgfNode> root = reader.get_tree_transfer_ownership();
+    PentobiTree tree(root);
+    unique_ptr<Board> 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> 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<SgfNode> root = reader.get_tree_transfer_ownership();
+    PentobiTree tree(root);
+    unique_ptr<Board> 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> 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 (file)
index 0000000..ef13284
--- /dev/null
@@ -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 "$<TARGET_FILE:pentobi>" 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 (file)
index 0000000..6963e5f
--- /dev/null
@@ -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 (file)
index 0000000..86b99c9
Binary files /dev/null and b/windows/blksgf.ico differ
diff --git a/windows/install.nsis.in b/windows/install.nsis.in
new file mode 100644 (file)
index 0000000..12ef4e9
--- /dev/null
@@ -0,0 +1,143 @@
+; Script for creating a Windows installer with NSIS (http://nsis.sf.net)\r
+\r
+!define PENTOBI_VERSION "@PENTOBI_VERSION@"\r
+!define PENTOBI_SRC_DIR "@CMAKE_SOURCE_DIR@"\r
+!define PENTOBI_BUILD_DIR "@CMAKE_BINARY_DIR@"\r
+\r
+!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\Pentobi"\r
+\r
+SetCompressor /SOLID lzma\r
+\r
+!define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\orange-install.ico"\r
+!define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\orange-uninstall.ico"\r
+!define MUI_WELCOMEFINISHPAGE_BITMAP "${NSISDIR}\Contrib\Graphics\Wizard\orange.bmp"\r
+!define MUI_COMPONENTSPAGE_NODESC\r
+!include "MUI.nsh"\r
+!insertmacro MUI_PAGE_WELCOME\r
+!insertmacro MUI_PAGE_LICENSE "${PENTOBI_SRC_DIR}\COPYING"\r
+!insertmacro MUI_PAGE_COMPONENTS\r
+!insertmacro MUI_PAGE_DIRECTORY\r
+!insertmacro MUI_PAGE_INSTFILES\r
+!define MUI_FINISHPAGE_RUN "$INSTDIR\Pentobi.exe"\r
+!insertmacro MUI_PAGE_FINISH\r
+!insertmacro MUI_UNPAGE_CONFIRM\r
+!insertmacro MUI_UNPAGE_INSTFILES\r
+\r
+!insertmacro MUI_LANGUAGE "English"\r
+!insertmacro MUI_LANGUAGE "German"\r
+!insertmacro MUI_RESERVEFILE_INSTALLOPTIONS\r
+\r
+!define ADD_START_MENU_ENTRY_DEFAULT "Add start menu entry"\r
+!define CREATE_DESKTOP_SHORTCUT_DEFAULT "Create desktop shortcut"\r
+!define INSTALLER_TITLE_DEFAULT "Pentobi ${PENTOBI_VERSION} Installer"\r
+LangString ADD_START_MENU_ENTRY ${LANG_ENGLISH} \\r
+  "${ADD_START_MENU_ENTRY_DEFAULT}"\r
+LangString CREATE_DESKTOP_SHORTCUT ${LANG_ENGLISH} \\r
+  "${CREATE_DESKTOP_SHORTCUT_DEFAULT}"\r
+LangString INSTALLER_TITLE ${LANG_ENGLISH} \\r
+  "${INSTALLER_TITLE_DEFAULT}"\r
+!include "${PENTOBI_SRC_DIR}\windows\German.nsh"\r
+\r
+Name "Pentobi"\r
+Caption "$(INSTALLER_TITLE)"\r
+OutFile "pentobi-${PENTOBI_VERSION}-install.exe"\r
+InstallDir "$PROGRAMFILES\Pentobi"\r
+InstallDirRegKey HKLM "Software\Pentobi" ""\r
+; Set admin level, needed for shortcut removal on Vista\r
+; (http://nsis.sf.net/Shortcuts_removal_fails_on_Windows_Vista)\r
+RequestExecutionLevel admin\r
+\r
+Section\r
+\r
+IfFileExists "$INSTDIR\Uninstall.exe" 0 +2\r
+ExecWait '"$INSTDIR\Uninstall.exe" /S _?=$INSTDIR'\r
+\r
+SetOutPath "$INSTDIR\translations"\r
+File "${PENTOBI_BUILD_DIR}\src\libpentobi_gui\*.qm"\r
+File "${PENTOBI_BUILD_DIR}\src\pentobi\*.qm"\r
+SetOutPath "$INSTDIR\books"\r
+File "${PENTOBI_SRC_DIR}\src\books\book_*.blksgf"\r
+SetOutPath "$INSTDIR"\r
+File /r "${PENTOBI_SRC_DIR}\src\pentobi\help"\r
+File /oname=COPYING.txt "${PENTOBI_SRC_DIR}\COPYING"\r
+File /oname=Pentobi.exe "${PENTOBI_BUILD_DIR}\windows\deploy\pentobi.exe"\r
+File "${PENTOBI_SRC_DIR}\src\pentobi\pentobi.ico"\r
+SetOutPath "$INSTDIR"\r
+File "${PENTOBI_BUILD_DIR}\windows\deploy\*.dll"\r
+SetOutPath "$INSTDIR\imageformats"\r
+File "${PENTOBI_BUILD_DIR}\windows\deploy\imageformats\*.dll"\r
+SetOutPath "$INSTDIR\platforms"\r
+File "${PENTOBI_BUILD_DIR}\windows\deploy\platforms\*.dll"\r
+SetOutPath "$INSTDIR\translations"\r
+File "${PENTOBI_BUILD_DIR}\windows\deploy\translations\qt_de.qm"\r
+\r
+WriteRegStr HKLM "Software\Pentobi" "" $INSTDIR\r
+\r
+WriteUninstaller $INSTDIR\Uninstall.exe\r
+WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "Pentobi"\r
+WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${PENTOBI_VERSION}"\r
+WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\pentobi.ico"\r
+WriteRegStr HKLM "${UNINST_KEY}" "URLInfoAbout" "http://pentobi.sf.net/"\r
+WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$INSTDIR\Uninstall.exe"\r
+\r
+SetOutPath "$INSTDIR"\r
+File "${PENTOBI_SRC_DIR}\windows\blksgf.ico"\r
+\r
+WriteRegStr HKCR ".blksgf" "" "Pentobi"\r
+WriteRegStr HKCR ".blksgf" "Content Type" "application/x-blokus-sgf"\r
+WriteRegStr HKCR "Pentobi" "" "Blokus Game"\r
+WriteRegStr HKCR "Pentobi\DefaultIcon" "" "$INSTDIR\blksgf.ico"\r
+WriteRegStr HKCR "Pentobi\shell\open\command" "" \\r
+  "$\"$INSTDIR\Pentobi.exe$\" $\"%1$\""\r
+\r
+WriteRegStr HKCR "MIME\Database\Content Type\application/x-blokus-sgf" \\r
+  "Extension" ".blksgf"\r
+\r
+WriteRegStr HKCR "Applications\Pentobi.exe" "SupportedTypes" ".blksgf"\r
+WriteRegStr HKCR "Applications\Pentobi\shell\open\command" "" \\r
+  "$\"$INSTDIR\Pentobi.exe$\" $\"%1$\""\r
+\r
+SectionEnd\r
+\r
+Section "$(ADD_START_MENU_ENTRY)"\r
+\r
+SetShellVarContext all\r
+CreateDirectory "$SMPROGRAMS\Games"\r
+CreateShortCut "$SMPROGRAMS\Games\Pentobi.lnk" "$INSTDIR\Pentobi.exe"\r
+\r
+SectionEnd\r
+\r
+Section "$(CREATE_DESKTOP_SHORTCUT)"\r
+\r
+SetShellVarContext all\r
+CreateShortCut "$DESKTOP\Pentobi.lnk" "$INSTDIR\Pentobi.exe"\r
+\r
+SectionEnd\r
+\r
+Section "Uninstall"\r
+\r
+Delete "$INSTDIR\Uninstall.exe"\r
+Delete "$INSTDIR\Pentobi.exe"\r
+Delete "$INSTDIR\COPYING.txt"\r
+Delete "$INSTDIR\pentobi.ico"\r
+Delete "$INSTDIR\blksgf.ico"\r
+Delete "$INSTDIR\*.dll"\r
+RmDir /r "$INSTDIR\books"\r
+RmDir /r "$INSTDIR\translations"\r
+RmDir /r "$INSTDIR\help"\r
+RmDir /r "$INSTDIR\platforms"\r
+RmDir /r "$INSTDIR\imageformats"\r
+RmDir /r "$INSTDIR\plugins"\r
+RmDir "$INSTDIR"\r
+\r
+SetShellVarContext all\r
+Delete "$SMPROGRAMS\Games\Pentobi.lnk"\r
+Delete "$DESKTOP\Pentobi.lnk"\r
+\r
+DeleteRegKey HKLM "Software\Pentobi"\r
+DeleteRegKey HKLM "${UNINST_KEY}"\r
+DeleteRegKey HKCR "Pentobi"\r
+DeleteRegKey HKCR "Applications\Pentobi.exe"\r
+DeleteRegKey HKCR "Applications\Pentobi"\r
+\r
+SectionEnd\r